├── .gitignore ├── CHANGES.md ├── README.md ├── README.old.md ├── bin ├── classpath ├── run └── test ├── boot.properties ├── build.boot ├── circle.yml ├── epl.html ├── examples ├── animation │ ├── index.html │ └── src │ │ └── core.cljs ├── counters │ ├── index.html │ └── src │ │ └── core.cljs ├── cursor_as_key │ ├── index.html │ └── src │ │ └── core.cljs ├── harmful │ ├── index.html │ └── src │ │ └── core.cljs ├── hello │ ├── index.html │ └── src │ │ └── core.cljs ├── input │ ├── index.html │ └── src │ │ └── core.cljs ├── instrument │ ├── index.html │ └── src │ │ └── core.cljs ├── mixins │ ├── index.html │ └── src │ │ └── core.cljs ├── mouse │ ├── index.html │ └── src │ │ └── core.cljs ├── multi │ ├── index.html │ └── src │ │ └── core.cljs ├── multiroot │ ├── index.html │ └── src │ │ └── core.cljs ├── refs │ ├── index.html │ └── src │ │ └── core.cljs ├── shared │ ├── index.html │ └── src │ │ └── core.cljs ├── sortable │ ├── index.html │ └── src │ │ └── core.cljs ├── state_bug │ ├── index.html │ └── src │ │ └── core.cljs ├── stateful │ ├── index.html │ └── src │ │ └── core.cljs ├── two_lists │ ├── index.html │ └── src │ │ └── core.cljs ├── typeahead │ ├── index.html │ └── src │ │ └── core.cljs ├── unmount │ ├── index.html │ └── src │ │ └── core.cljs ├── update_props │ ├── index.html │ └── src │ │ └── core.cljs └── verify │ ├── index.html │ └── src │ └── core.cljs ├── pom.xml ├── project.clj ├── resources ├── index.html └── public │ └── devcards │ ├── index.html │ └── main.cljs.edn ├── script ├── build.clj ├── figwheel.clj ├── index.html ├── repl.clj ├── test.clj └── watch.clj └── src ├── devcards └── om │ └── devcards │ ├── autocomplete.cljs │ ├── bugs.cljs │ ├── core.cljs │ ├── shared_fn_test.cljs │ ├── tutorials.cljs │ └── utils.cljs ├── main ├── data_readers.clj ├── deps.cljs └── om │ ├── checksums.clj │ ├── core.clj │ ├── core.cljs │ ├── dom.cljc │ ├── dom.cljs │ ├── externs.js │ ├── impl.cljs │ ├── next.cljc │ ├── next │ ├── cache.cljs │ ├── impl │ │ └── parser.cljc │ ├── protocols.cljc │ └── server.clj │ ├── tempid.cljc │ ├── transit.cljc │ └── util.cljc └── test └── om ├── checksums_test.clj ├── dom_test.clj ├── next ├── next_test.clj ├── run_tests.cljs ├── tests.cljc └── tutorials_test.clj ├── test_utils.clj └── tests.cljs /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | /out 3 | /target 4 | /classes 5 | /checkouts 6 | pom.xml.asc 7 | *.map 8 | *.js 9 | *.jar 10 | *.class 11 | /.lein-* 12 | /.nrepl-port 13 | examples/history 14 | *init.clj 15 | .repl* 16 | .idea 17 | om.iml 18 | node_modules 19 | .cljs_node_repl -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | ## 0.8.8 2 | 3 | ### Changes 4 | * Leverage 0.0-2755 macro usage enhancements 5 | 6 | ## 0.8.7 7 | 8 | ### Changes 9 | * Rely on newer cljsjs.react artifact 10 | 11 | ## 0.8.6 12 | 13 | ### Changes 14 | * Rely on cljsjs.react artifact 15 | 16 | ## 0.8.5 17 | 18 | ### Fixes 19 | * Fix subtle rendering bug in code set-state! queue logic 20 | 21 | ## 0.8.4 22 | 23 | ### Changes 24 | * Bump dependencies 25 | 26 | ## 0.8.3 27 | 28 | ### Changes 29 | * Bump dependencies 30 | 31 | ## 0.8.2 32 | 33 | ### Changes 34 | * Bump dependencies 35 | 36 | ## 0.8.1 37 | 38 | ### Changes 39 | * Proper fix to OM-300 40 | * Bump React JAR dep to 0.12.2.2 41 | 42 | ## 0.8.0 43 | 44 | ### Changes 45 | * OM-307: get-props extended to have a two arity form (korks) 46 | * OM-306: dissoc :raf 47 | 48 | ## 0.8.0-rc1 49 | 50 | ### Changes 51 | * relax transact! precondition, only require ITransact instance 52 | 53 | ### Fixes 54 | * OM-301: Deprecation warning for om.dom/input 55 | * OM-300: do not preserve component local state entry after unmount 56 | 57 | ## 0.8.0-beta5 58 | 59 | ### Changes 60 | * Depend on 0.12.2.1 React JAR with new externs 61 | 62 | ## 0.8.0-beta4 63 | 64 | ### Changes 65 | * Add low-level namespace om.impl 66 | * update to React 0.12.2 67 | 68 | ### Fixes 69 | * OM-190: add :key-fn option to `om.core/build` 70 | * OM-253: overrideable rAF 71 | * OM-294: IKVReduce for MapCursor & IndexedCursor 72 | * OM-296: `no-local-merge-pending-state` was not setting `:previous-state` 73 | 74 | ## 0.8.0-beta3 75 | 76 | ### Fixes 77 | * OM-290: component? precondition unexpectedly failing 78 | 79 | ## 0.8.0-beta2 80 | 81 | ### Enhancements 82 | * Preconditions on most of the public api to support earlier failures 83 | * Make `render-all` public 84 | 85 | ## 0.8.0-beta1 86 | 87 | ### Enhancements 88 | * Improved multimethod support, mount/unmount life-cycle methods invoked 89 | as expected 90 | 91 | ## 0.8.0-alpha2 92 | 93 | ### Enhancements 94 | * OM-260: remove cursor consistency checks 95 | 96 | ### Fixes 97 | * OM-276: bad pending state for no local state 98 | * OM-262: input behavior regression 99 | * OM-270: incorrect no-local state behavior 100 | * OM-274: incorrect -lookup behavior for IndexedCursor 101 | * OM-267: bad logic for not found case in MapCusor -lookup implementation 102 | * OM-271: typo, parent not passed to ref sub-cursor 103 | 104 | ## 0.8.0-alpha1 105 | 106 | ### Enhancements 107 | * reference cursors 108 | * om.core/commit!, like om.core/transact! but will not trigger a re-render 109 | * add marquee tag 110 | * add om.core/mounted? 111 | * experimental support to write component local state into global state 112 | 113 | ### Changes 114 | * React 0.11.2 115 | 116 | ### Fixes 117 | * om.core/root now properly returns mounted component 118 | * default shouldComponentUpdate now compares state 119 | 120 | ## 0.7.3 121 | 122 | ### Changes 123 | * OM-243: fix regression in supporting components built from non-cursors 124 | 125 | ## 0.7.2 126 | 127 | ### Changes 128 | * OM-239: update components if cursor path changes 129 | 130 | ## 0.7.1 131 | 132 | ### Changes 133 | * OM-133: validate Om component fn return values 134 | * OM-134: add om.core/set-state-nr! and om.core/update-state-nr!, they do not refresh (experimental) 135 | * OM-162: extend default cursor to IEmptyableCollection (experimental) 136 | * OM-180: add om.core/detach-root to remove Om render loop 137 | * OM-214: Cursors should implement IHash 138 | 139 | ## 0.7.0 140 | 141 | ### Changes 142 | * BREAKING: :ctor options deprecated for :descriptor see docs 143 | * depend on ClojureScript 0.0-2277 144 | * depend on React 0.11.1 145 | 146 | Fixes 147 | ### 148 | * OM-170: :tx-listen registration issues 149 | 150 | ## 0.6.5 151 | 152 | ### Changes 153 | * depend on Clojure 1.6.0 and ClojureScript 0.0-2268 154 | 155 | ## 0.6.4 156 | 157 | ### Changes 158 | * depend on 0.9.0.2 React CLJS 159 | 160 | ## 0.6.3 161 | 162 | ### Enhancements 163 | * added ellipse tag 164 | 165 | ### Bug Fixes 166 | * OM-179: prevent forceUpdate() if component not mounted 167 | * OM-175: error on render-to-str with set-state! 168 | 169 | ## 0.6.2 170 | 171 | ### Bug Fixes 172 | * Stale code around component local state 173 | 174 | ## 0.6.1 175 | 176 | ### Enhancements 177 | * Pure shouldComponentUpdate logic now uses equiv instead of identical? 178 | * add om.core/rendering? predicate to detect React render phase 179 | * more sensible handling of component local state w/o internal graft, 180 | we now rely on React's forceUpdate 181 | * add ICursorDerive protocol 182 | 183 | ### Bug Fixes 184 | * OM-155: multiroot and & :tx-listen incompatible 185 | * OM-152: pass ::index to :fn if available 186 | * OM-150: add missing SVG tags 187 | 188 | ## 0.6.0 189 | 190 | ### Breaking Changes 191 | * OM-152: eliminate om.core/graft 192 | 193 | ### Changes 194 | * added ICursorDerive protocol 195 | * document :ctor option to om.core/build 196 | * add om.core/rendering? predicate 197 | * pass ::index to :fn if available in om.core/build 198 | 199 | ### Fixes 200 | * OM-150: missing SVG tags 201 | * OM-155: multiroot & :tx-listen incompatible 202 | 203 | ## 0.5.3 204 | 205 | ### Changes 206 | * added pure-methods for reuse 207 | * added specify-state-methods! for reuse 208 | * can explicitly give a component an id via :om.core/id with :init-state 209 | 210 | ## 0.5.2 211 | 212 | ### Enhancements 213 | * OM-127: om.core/refresh! 214 | * OM-28: om.core/update-state! 215 | * protocols for local state 216 | * Pure implementation methods now provided in immutable map 217 | 218 | ## 0.5.1 219 | 220 | ### Changes 221 | * Requires ClojureScript 0.0-2173 222 | * IOmSwap protocol for app state representations besides standard atom 223 | * :instrument option for om.core/root 224 | * Add IDisplayName to support React Chrome Dev Tools 225 | * om.core/build takes :ctor option to provide React backing component 226 | class besides om.core/Pure 227 | 228 | ### Fixes 229 | * OM-122: IWillReceiveProps protocol 230 | 231 | ## 0.5.0 232 | 233 | ### Changes 234 | * Upgrade to React 0.9.0 235 | 236 | ## 0.5.0-rc1 237 | 238 | ### Breaking Changes 239 | * IDidMount, IDidUpdate no longer take node to match React 0.9.0-rc1 240 | 241 | ### Enhancements 242 | * om.dom/render-to-str added 243 | * OM-72: :path option for om.core/root 244 | 245 | ## 0.4.2 246 | 247 | ### Enhancements 248 | * OM-112: transact! special cases when korks is nil 249 | 250 | ### Bug fixes 251 | * OM-114: om.core/update! doesn't pass tag 252 | 253 | ## 0.4.1 254 | 255 | ### Bug fixes 256 | * OM-110: transact! broken for empty path 257 | 258 | ## 0.4.0 259 | 260 | ### Breaking Changes 261 | * `om.core/root` signature changed to be more like `om.core/build` 262 | * `om.core/update!` no longer takes keys, or function, or function args 263 | * `om.core/transact!` no longer takes arguments after function 264 | 265 | ### Enhancements 266 | * `om.core/transact!` now takes optional 4th argument, a tag 267 | * `om.core/update!` now takes optional 4th argument, a tag 268 | * `om.core/root` now supports `:tx-listen` option to observe all transactions 269 | 270 | ### Bug fixes 271 | * `om.core/shared` was brittle, now works without cursors 272 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Om 2 | 3 | > *NOTE*: This project is no longer under active development. If you'd like to use 4 | > a library that's well maintained that was inspired by some of the ideas 5 | > presented here see [Fulcro](https://github.com/fulcrologic/fulcro) 6 | 7 | A [ClojureScript](http://github.com/clojure/clojurescript) UI framework and 8 | client/server architecture over [Facebook's 9 | React](http://facebook.github.io/react/). 10 | 11 | Om UIs are out of the box snapshotable and undoable and these operations have 12 | no implementation complexity and little overhead. 13 | 14 | Om borrows ideas liberally from [Facebook's 15 | Relay](https://facebook.github.io/relay/) and [Netflix's 16 | Falcor](http://netflix.github.io/falcor/) with a dash of inspiration from 17 | [Datomic pull syntax](http://docs.datomic.com/pull.html) to avoid the typical 18 | incidental complexity that arises from client/server state management. 19 | 20 | ## Dependency Information 21 | 22 | Latest release: 1.0.0-beta1 23 | 24 | [Leiningen](http://github.com/technomancy/leiningen/) and [Boot](http://boot-clj.com) 25 | dependency information: 26 | 27 | ``` 28 | [org.omcljs/om "1.0.0-beta1"] 29 | ``` 30 | 31 | [Maven](http://maven.apache.org) dependency information: 32 | 33 | ``` 34 | <dependency> 35 | <groupId>org.omcljs</groupId> 36 | <artifactId>om</artifactId> 37 | <version>1.0.0-beta1</version> 38 | </dependency> 39 | ``` 40 | 41 | ## Example 42 | 43 | ```clojure 44 | (ns example 45 | (:require [goog.dom :as gdom] 46 | [om.dom :as dom] 47 | [om.next :as om :refer [defui]])) 48 | 49 | (defui Hello 50 | Object 51 | (render [this] 52 | (dom/h1 nil "Hello, world!"))) 53 | 54 | (def hello (om/factory Hello)) 55 | 56 | (.render js/ReactDOM (hello) (gdom/getElement "example")) 57 | ``` 58 | 59 | ## Tutorials 60 | 61 | There is an Quick Start tutorial that will introduce you to the core 62 | concepts of Om 63 | [here](https://github.com/omcljs/om/wiki/Quick-Start-%28om.next%29). There are 64 | also a variety of other guides [here](https://github.com/omcljs/om/wiki#om-next). 65 | 66 | ## Documentation 67 | 68 | There is documentation [here](https://github.com/omcljs/om/wiki/Documentation-%28om.next%29) 69 | 70 | ## Contributing 71 | 72 | Please contact me via email to request an electronic Contributor 73 | Agreement. Once your electronic CA has been signed and returned to me 74 | I will accept pull requests. 75 | 76 | ## Community 77 | 78 | If you are looking for help please get in touch either on the 79 | [clojurians.slack.com **#om** channel](http://clojurians.net) or the 80 | [om-cljs Google Group](https://groups.google.com/d/forum/om-cljs). 81 | 82 | ## References 83 | 84 | * [Worlds: Controlling the Scope of Side Effects](http://www.vpri.org/pdf/tr2011001_final_worlds.pdf) 85 | * [A Functional I/O System](http://www.ccs.neu.edu/racket/pubs/icfp09-fffk.pdf) 86 | * [Directness and Liveness in the Morphic User Interface Construction Environment](http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.103.600&rep=rep1&type=pdf) 87 | * [Learnable Programming](http://worrydream.com/LearnableProgramming/) 88 | * [Relay](https://facebook.github.io/relay/) 89 | * [Falcor](http://netflix.github.io/falcor/) 90 | * [GraphQL](http://graphql.org) 91 | * [Datomic pull syntax](http://docs.datomic.com/pull.html) 92 | 93 | ## Copyright and license 94 | 95 | Copyright © 2013-2017 David Nolen 96 | 97 | Licensed under the EPL (see the file epl.html). 98 | -------------------------------------------------------------------------------- /README.old.md: -------------------------------------------------------------------------------- 1 | # Om 2 | 3 | A [ClojureScript](http://github.com/clojure/clojurescript) interface 4 | to [Facebook's React](http://facebook.github.io/react/). 5 | 6 | Om allows users to represent their UIs simply as 7 | [EDN](http://github.com/edn-format/edn). Because ClojureScript data is 8 | immutable data, Om can always rapidly re-render the UI from the 9 | root. Thus Om UIs are out of the box snapshotable and undoable and 10 | these operations have no implementation complexity and little 11 | overhead. 12 | 13 | [See](http://swannodette.github.io/todomvc/labs/architecture-examples/om-undo/index.html) 14 | for [yourself](http://swannodette.github.io/2013/12/31/time-travel). 15 | 16 | ## Unique Features 17 | 18 | Om supports features not currently present in React: 19 | 20 | * Global state management facilities built in 21 | * Components may have arbitrary data dependencies, not limited to props & state 22 | * Component construction can be intercepted via 23 | `:instrument`. Simplifies debugging components and generic editors. 24 | * Provides stream of all application state change deltas via 25 | `:tx-listen`. Simplifies synchronization online and offline. 26 | * Customizable semantics. Fine grained control over how components store 27 | state, even for components outside of your control. Simplifies using 28 | Om components outside the Om framework, debugging, and adding event 29 | hooks not anticipated by original component designer. 30 | 31 | ## Tutorials 32 | 33 | There is an in-depth tutorial that will introduce you to the core 34 | concepts of Om 35 | [here](http://github.com/swannodette/om/wiki/Basic-Tutorial) and a 36 | real-world integration example 37 | [here](http://github.com/swannodette/om/wiki/Intermediate-Tutorial). The 38 | community maintained [om-cookbook](https://github.com/omcljs/om-cookbook) 39 | covers many common idioms and patterns. 40 | 41 | ## Examples 42 | 43 | ```clojure 44 | (ns example 45 | (:require [om.core :as om] 46 | [om.dom :as dom])) 47 | 48 | (defn widget [data owner] 49 | (reify 50 | om/IRender 51 | (render [this] 52 | (dom/h1 nil (:text data))))) 53 | 54 | (om/root widget {:text "Hello world!"} 55 | {:target (. js/document (getElementById "my-app"))}) 56 | ``` 57 | 58 | The repo includes several simple examples you can build yourself. If 59 | you view the `project.clj` you will see their build 60 | identifiers. Assuming you have [Leiningen](http://leiningen.org/) 61 | installed, to build an example run: 62 | 63 | ``` 64 | lein cljsbuild once <build-id> 65 | ``` 66 | 67 | Then open the corresponding `index.html` in your favorite browser. 68 | 69 | For a more fleshed-out example, please see the Om implementation of 70 | [TodoMVC](http://todomvc.com) 71 | [exists here](http://github.com/swannodette/todomvc/blob/gh-pages/labs/architecture-examples/om/src/todomvc/app.cljs). 72 | 73 | ## Documentation 74 | 75 | There is documentation [here](http://github.com/swannodette/om/wiki/Documentation). 76 | 77 | There is also a 78 | [conceptual overview](http://github.com/swannodette/om/wiki/Conceptual-overview) 79 | that we recommend reading as there are some design choices in Om that 80 | make it quite different from other client side solutions and even 81 | React itself. 82 | 83 | ## Reusable Components 84 | 85 | Om emphasizes building modular and adaptable components. Some 86 | examples: 87 | 88 | * [om-bootstrap](https://github.com/racehub/om-bootstrap), Bootstrap 3 Om Components 89 | * [ankha](http://github.com/noprompt/ankha), an EDN inspector view 90 | * [om-draggable](https://github.com/sgrove/om-draggable), generic 91 | draggable 92 | * [om-autocomplete](https://github.com/arosequist/om-autocomplete), 93 | customizable autocompleter 94 | * [ff-om-draggable](https://github.com/neo/ff-om-draggable) 95 | * [om-widgets](https://bitbucket.org/athieme/om-widgets) 96 | * [om-dev-component](https://github.com/ioRekz/om-dev-component), add dev features (e.g. state history navigation) to your component 97 | * [om-sync](http://github.com/swannodette/om-sync), keep client and 98 | server in sync (experimental) 99 | 100 | ## Applications built with Om 101 | 102 | * [Project FiFo](https://blog.project-fifo.net/the-stack-we-choose-erlang-smartos-clojure/), a SmartOS cloud orchestration platform 103 | * [Recurse Center Community](https://github.com/hackerschool/community) 104 | * [Framed](http://www.framed.io/) 105 | * [Netrunner](https://github.com/mtgred/netrunner) 106 | * [CircleCI](http://www.circleci.com/), source [here](https://github.com/circleci/frontend) 107 | * [Precursor](https://precursorapp.com) 108 | * [Assistant](https://github.com/29decibel/assistant) 109 | * [Fitsme](http://fitsmeapp.com) 110 | * [Goya](http://jackschaedler.github.io/goya/), pixel editor with 111 | undo/redo and visual history 112 | * [AppShare](https://github.com/zubairq/AppShare), a Clojure web framework 113 | * [wordsmith](http://wordsmith.variadic.me), a markdown editor 114 | * [omchaya](http://github.com/sgrove/omchaya) 115 | * [BVCA Private Equity Map](http://bvca.clustermap.trampolinesystems.com/) 116 | * [session](http://github.com/kovasb/session) 117 | * [pOModoro](http://pomodoro.trevorlandau.net) 118 | * [Dakait](http://github.com/verma/dakait), a web-based tool to manage 119 | downloads 120 | * [Mega Super Mario World](http://github.com/city41/mario-review), a detailed review of the classic video game and a SNES video editor 121 | * [Time for Coffee!](http://www.timeforcoffee.ch), a handy website to display the next departures at public transport stops in Switzerland 122 | * [Omingard](https://omingard.5apps.com), a Solitaire-like card game. Making-of: [My Way into Clojure](http://www.railslove.com/stories/my-way-into-clojure-building-a-card-game-with-om-part-1). 123 | * [Horizon Alpha](https://github.com/BertrandDechoux/horizon-alpha), a quick Hack and slash game using the Noob universe 124 | * [Solari Architects](http://solariarchitects.com/), portfolio for architecture firm. 125 | 126 | ## Using it 127 | 128 | The current version depends on React 0.13.3. 129 | 130 | Make sure you have [Leiningen](http://leiningen.org/) installed. 131 | 132 | Your `project.clj` should include something like the following: 133 | 134 | ```clojure 135 | (defproject foo "0.1.0" 136 | ... 137 | :dependencies [[org.clojure/clojure "1.6.0"] 138 | [org.clojure/clojurescript "0.0-2760"] 139 | [org.omcljs/om "0.9.0"]] 140 | ...) 141 | ``` 142 | 143 | ### React with Add-Ons 144 | 145 | If you would rather use React with Add-Ons you can configure this 146 | with Maven's exclusions feature: 147 | 148 | ```clojure 149 | (defproject foo "0.1.0" 150 | ... 151 | :dependencies [[org.clojure/clojure "1.6.0"] 152 | [org.clojure/clojurescript "0.0-2760"] 153 | [org.omcljs/om "0.9.0" :exclusions [cljsjs/react]] 154 | [cljsjs/react-with-addons "0.13.3-0"]] 155 | ...) 156 | ``` 157 | 158 | ### Build configuration 159 | 160 | For local development your 161 | [lein-cljsbuild](http://github.com/emezeske/lein-cljsbuild) settings 162 | should look something like this: 163 | 164 | ```clojure 165 | :cljsbuild { 166 | :builds [{:id "dev" 167 | :source-paths ["src"] 168 | :compiler { 169 | :main main.core 170 | :output-to "main.js" 171 | :output-dir "out" 172 | :optimizations :none 173 | :source-map true}}]} 174 | ``` 175 | 176 | Your markup should look something like the following: 177 | 178 | ```html 179 | <html> 180 | <body> 181 | <div id="my-app"></div> 182 | <script src="main.js" type="text/javascript"></script> 183 | </body> 184 | </html> 185 | ``` 186 | 187 | For production your [lein-cljsbuild](http://github.com/emezeske/lein-cljsbuild) settings should look something 188 | like this: 189 | 190 | ```clojure 191 | :cljsbuild { 192 | :builds [{:id "release" 193 | :source-paths ["src"] 194 | :compiler { 195 | :main main.core 196 | :output-to "main.js" 197 | :optimizations :advanced 198 | :pretty-print false}}]} 199 | ``` 200 | 201 | ## Contributing 202 | 203 | Please contact me via email to request an electronic Contributor 204 | Agreement. Once your electronic CA has been signed and returned to me 205 | I will accept pull requests. 206 | 207 | ## Community 208 | 209 | If you are looking for help please get in touch either on the 210 | [clojurians.slack.com **#om** channel](http://clojurians.net) or the 211 | [om-cljs Google Group](https://groups.google.com/d/forum/om-cljs). 212 | 213 | ## FAQ 214 | 215 | ### Can I use a different HTML Syntax? 216 | 217 | Om is not opinionated about HTML syntax, third parties can provide the 218 | preferred flavors over the `React.DOM` api. Alternative syntaxes will 219 | be listed here: 220 | 221 | * [sablono](http://github.com/r0man/sablono), Hiccup-style 222 | * [kioo](http://github.com/ckirkendall/kioo), Enlive-style 223 | 224 | ### Does Om provide routing? 225 | 226 | Om does not ship with a router and is unlikely to. However 227 | ClojureScript routing libraries exist that handle this problem quite 228 | well: 229 | 230 | * [secretary](http://github.com/gf3/secretary) 231 | * [silk](http://github.com/DomKM/silk) 232 | * [bidi](http://github.com/juxt/bidi) 233 | 234 | ### How do I test Om programs? 235 | 236 | * Sean Grove's [omchaya](http://github.com/sgrove/omchaya) is a good 237 | starting point for understanding common testing patterns 238 | * There are some notes [here](http://github.com/swannodette/om/wiki/Testing) 239 | 240 | ## References 241 | 242 | * [Worlds: Controlling the Scope of Side Effects](http://www.vpri.org/pdf/tr2011001_final_worlds.pdf) 243 | * [A Functional I/O System](http://www.ccs.neu.edu/racket/pubs/icfp09-fffk.pdf) 244 | * [Directness and Liveness in the Morphic User Interface Construction Environment](http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.103.600&rep=rep1&type=pdf) 245 | * [Learnable Programming](http://worrydream.com/LearnableProgramming/) 246 | 247 | ## Copyright and license 248 | 249 | Copyright © 2013-2017 David Nolen 250 | 251 | Licensed under the EPL (see the file epl.html). 252 | -------------------------------------------------------------------------------- /bin/classpath: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | classpath_file=classpath.txt 6 | 7 | mvn -q dependency:build-classpath -Dmdep.outputFile=$classpath_file 8 | 9 | (echo ":`pwd`/src/main:`pwd`/src/test:`pwd`/src/resources:`pwd`/target/classes") >> $classpath_file 10 | -------------------------------------------------------------------------------- /bin/run: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | java -cp `cat classpath.txt` clojure.main $@ -------------------------------------------------------------------------------- /bin/test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | bin/classpath 6 | java -cp `cat classpath.txt` clojure.main script/test.clj 7 | node target/test/test.js -------------------------------------------------------------------------------- /boot.properties: -------------------------------------------------------------------------------- 1 | #http://boot-clj.com 2 | #Fri Sep 02 22:34:38 WEST 2016 3 | BOOT_CLOJURE_NAME=org.clojure/clojure 4 | BOOT_CLOJURE_VERSION=1.9.0-alpha19 5 | BOOT_VERSION=2.7.2 6 | -------------------------------------------------------------------------------- /build.boot: -------------------------------------------------------------------------------- 1 | (set-env! 2 | :source-paths #{"src/main"} 3 | :dependencies '[[org.clojure/clojure "1.9.0-alpha19" :scope "provided"] 4 | [org.clojure/clojurescript "1.9.908" :scope "provided" 5 | :classifier "aot" 6 | :exclusions [org.clojure/clojure 7 | org.clojure/data.json]] 8 | [org.clojure/clojurescript "1.9.908" :scope "provided" 9 | :exclusions [org.clojure/clojure 10 | org.clojure/data.json]] 11 | [org.clojure/data.json "0.2.6" :scope "provided" 12 | :classifier "aot"] 13 | [cljsjs/react "16.0.0-0"] 14 | [cljsjs/react-dom "16.0.0-0"] 15 | [com.cognitect/transit-clj "0.8.300"] 16 | [com.cognitect/transit-cljs "0.8.239"] 17 | 18 | [org.clojure/core.async "0.3.443" :scope "test" 19 | :exclusions [org.clojure/tools.reader]] 20 | [figwheel-sidecar "0.5.10" :scope "test" 21 | :exclusions [org.clojure/clojurescript 22 | org.clojure/tools.reader]] 23 | [devcards "0.2.4-SNAPSHOT" :scope "test" 24 | :exclusions [org.clojure/clojurescript]] 25 | [com.cemerick/piggieback "0.2.1" :scope "test" 26 | :exclusions [org.clojure/clojure 27 | org.clojure/tools.nrepl 28 | org.clojure/clojurescript]] 29 | [pandeiro/boot-http "0.8.3" :scope "test"] 30 | [adzerk/boot-cljs "2.1.4" :scope "test" 31 | :exclusions [org.clojure/clojurescript]] 32 | [adzerk/boot-cljs-repl "0.3.3" :scope "test"] 33 | [adzerk/boot-test "1.2.0" :scope "test"] 34 | [crisptrutski/boot-cljs-test "0.3.4" :scope "test" 35 | :exclusions [org.clojure/clojurescript]] 36 | [adzerk/boot-reload "0.5.2" :scope "test"] 37 | [org.clojure/tools.nrepl "0.2.13" :scope "test"] 38 | [weasel "0.7.0" :scope "test"]] 39 | :exclusions '[org.clojure/clojure org.clojure/clojurescript]) 40 | 41 | (require 42 | '[adzerk.boot-cljs :refer [cljs]] 43 | '[adzerk.boot-cljs-repl :as cr :refer [cljs-repl start-repl]] 44 | '[adzerk.boot-reload :refer [reload]] 45 | '[adzerk.boot-test :as bt] 46 | '[crisptrutski.boot-cljs-test :as bct] 47 | '[pandeiro.boot-http :refer [serve]]) 48 | 49 | (deftask devcards [] 50 | (set-env! :source-paths #(conj % "src/devcards") 51 | :resource-paths #{"resources/public"}) 52 | (comp 53 | ;; remove possible artifacts of Figwheel compilation 54 | (sift :include #{#"^devcards\/(out\/|main.js)"} :invert true) 55 | (serve :port 3449) 56 | (watch) 57 | (cljs-repl) 58 | (reload) 59 | (speak) 60 | (cljs :source-map true 61 | :optimizations :none 62 | :compiler-options {:devcards true 63 | :main 'om.devcards.core 64 | :verbose true 65 | :parallel-build true} 66 | :ids #{"devcards/main"}) 67 | (target))) 68 | 69 | (deftask testing [] 70 | (set-env! :source-paths #(conj % "src/test")) 71 | identity) 72 | 73 | (ns-unmap 'boot.user 'test) 74 | 75 | (deftask test-clj [] 76 | (comp 77 | (testing) 78 | (bt/test))) 79 | 80 | (deftask test-cljs 81 | [e exit? bool "Enable flag."] 82 | (let [exit? (cond-> exit? 83 | (nil? exit?) not)] 84 | (comp 85 | (testing) 86 | (bct/test-cljs 87 | :js-env :node 88 | :namespaces #{'om.next.tests} 89 | :cljs-opts {:parallel-build true 90 | :target :nodejs 91 | :asset-path "test_suite.out"} 92 | :exit? exit? 93 | :ids #{"lumo_test/test_suite"})))) 94 | 95 | (deftask test 96 | [e exit? bool "Enable flag."] 97 | (let [exit? (cond-> exit? 98 | (nil? exit?) not)] 99 | (comp 100 | (test-clj) 101 | (test-cljs :exit? exit?)))) 102 | 103 | (deftask auto-test [] 104 | (comp 105 | (watch) 106 | (speak) 107 | (test :exit? false))) 108 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | java: 3 | version: oraclejdk8 4 | 5 | dependencies: 6 | pre: 7 | - wget https://github.com/boot-clj/boot-bin/releases/download/latest/boot.sh 8 | - mv boot.sh boot && chmod a+x boot && sudo mv boot /usr/local/bin 9 | override: 10 | - boot > /dev/null 11 | 12 | test: 13 | override: 14 | - boot test 15 | -------------------------------------------------------------------------------- /epl.html: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="ISO-8859-1" ?> 2 | <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> 3 | <html xmlns="http://www.w3.org/1999/xhtml"> 4 | 5 | <head> 6 | <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1" /> 7 | <title>Eclipse Public License - Version 1.0</title> 8 | <style type="text/css"> 9 | body { 10 | size: 8.5in 11.0in; 11 | margin: 0.25in 0.5in 0.25in 0.5in; 12 | tab-interval: 0.5in; 13 | } 14 | p { 15 | margin-left: auto; 16 | margin-top: 0.5em; 17 | margin-bottom: 0.5em; 18 | } 19 | p.list { 20 | margin-left: 0.5in; 21 | margin-top: 0.05em; 22 | margin-bottom: 0.05em; 23 | } 24 | </style> 25 | 26 | </head> 27 | 28 | <body lang="EN-US"> 29 | 30 | <h2>Eclipse Public License - v 1.0</h2> 31 | 32 | <p>THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE 33 | PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR 34 | DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS 35 | AGREEMENT.</p> 36 | 37 | <p><b>1. DEFINITIONS</b></p> 38 | 39 | <p>"Contribution" means:</p> 40 | 41 | <p class="list">a) in the case of the initial Contributor, the initial 42 | code and documentation distributed under this Agreement, and</p> 43 | <p class="list">b) in the case of each subsequent Contributor:</p> 44 | <p class="list">i) changes to the Program, and</p> 45 | <p class="list">ii) additions to the Program;</p> 46 | <p class="list">where such changes and/or additions to the Program 47 | originate from and are distributed by that particular Contributor. A 48 | Contribution 'originates' from a Contributor if it was added to the 49 | Program by such Contributor itself or anyone acting on such 50 | Contributor's behalf. Contributions do not include additions to the 51 | Program which: (i) are separate modules of software distributed in 52 | conjunction with the Program under their own license agreement, and (ii) 53 | are not derivative works of the Program.</p> 54 | 55 | <p>"Contributor" means any person or entity that distributes 56 | the Program.</p> 57 | 58 | <p>"Licensed Patents" mean patent claims licensable by a 59 | Contributor which are necessarily infringed by the use or sale of its 60 | Contribution alone or when combined with the Program.</p> 61 | 62 | <p>"Program" means the Contributions distributed in accordance 63 | with this Agreement.</p> 64 | 65 | <p>"Recipient" means anyone who receives the Program under 66 | this Agreement, including all Contributors.</p> 67 | 68 | <p><b>2. GRANT OF RIGHTS</b></p> 69 | 70 | <p class="list">a) Subject to the terms of this Agreement, each 71 | Contributor hereby grants Recipient a non-exclusive, worldwide, 72 | royalty-free copyright license to reproduce, prepare derivative works 73 | of, publicly display, publicly perform, distribute and sublicense the 74 | Contribution of such Contributor, if any, and such derivative works, in 75 | source code and object code form.</p> 76 | 77 | <p class="list">b) Subject to the terms of this Agreement, each 78 | Contributor hereby grants Recipient a non-exclusive, worldwide, 79 | royalty-free patent license under Licensed Patents to make, use, sell, 80 | offer to sell, import and otherwise transfer the Contribution of such 81 | Contributor, if any, in source code and object code form. This patent 82 | license shall apply to the combination of the Contribution and the 83 | Program if, at the time the Contribution is added by the Contributor, 84 | such addition of the Contribution causes such combination to be covered 85 | by the Licensed Patents. The patent license shall not apply to any other 86 | combinations which include the Contribution. No hardware per se is 87 | licensed hereunder.</p> 88 | 89 | <p class="list">c) Recipient understands that although each Contributor 90 | grants the licenses to its Contributions set forth herein, no assurances 91 | are provided by any Contributor that the Program does not infringe the 92 | patent or other intellectual property rights of any other entity. Each 93 | Contributor disclaims any liability to Recipient for claims brought by 94 | any other entity based on infringement of intellectual property rights 95 | or otherwise. As a condition to exercising the rights and licenses 96 | granted hereunder, each Recipient hereby assumes sole responsibility to 97 | secure any other intellectual property rights needed, if any. For 98 | example, if a third party patent license is required to allow Recipient 99 | to distribute the Program, it is Recipient's responsibility to acquire 100 | that license before distributing the Program.</p> 101 | 102 | <p class="list">d) Each Contributor represents that to its knowledge it 103 | has sufficient copyright rights in its Contribution, if any, to grant 104 | the copyright license set forth in this Agreement.</p> 105 | 106 | <p><b>3. REQUIREMENTS</b></p> 107 | 108 | <p>A Contributor may choose to distribute the Program in object code 109 | form under its own license agreement, provided that:</p> 110 | 111 | <p class="list">a) it complies with the terms and conditions of this 112 | Agreement; and</p> 113 | 114 | <p class="list">b) its license agreement:</p> 115 | 116 | <p class="list">i) effectively disclaims on behalf of all Contributors 117 | all warranties and conditions, express and implied, including warranties 118 | or conditions of title and non-infringement, and implied warranties or 119 | conditions of merchantability and fitness for a particular purpose;</p> 120 | 121 | <p class="list">ii) effectively excludes on behalf of all Contributors 122 | all liability for damages, including direct, indirect, special, 123 | incidental and consequential damages, such as lost profits;</p> 124 | 125 | <p class="list">iii) states that any provisions which differ from this 126 | Agreement are offered by that Contributor alone and not by any other 127 | party; and</p> 128 | 129 | <p class="list">iv) states that source code for the Program is available 130 | from such Contributor, and informs licensees how to obtain it in a 131 | reasonable manner on or through a medium customarily used for software 132 | exchange.</p> 133 | 134 | <p>When the Program is made available in source code form:</p> 135 | 136 | <p class="list">a) it must be made available under this Agreement; and</p> 137 | 138 | <p class="list">b) a copy of this Agreement must be included with each 139 | copy of the Program.</p> 140 | 141 | <p>Contributors may not remove or alter any copyright notices contained 142 | within the Program.</p> 143 | 144 | <p>Each Contributor must identify itself as the originator of its 145 | Contribution, if any, in a manner that reasonably allows subsequent 146 | Recipients to identify the originator of the Contribution.</p> 147 | 148 | <p><b>4. COMMERCIAL DISTRIBUTION</b></p> 149 | 150 | <p>Commercial distributors of software may accept certain 151 | responsibilities with respect to end users, business partners and the 152 | like. While this license is intended to facilitate the commercial use of 153 | the Program, the Contributor who includes the Program in a commercial 154 | product offering should do so in a manner which does not create 155 | potential liability for other Contributors. Therefore, if a Contributor 156 | includes the Program in a commercial product offering, such Contributor 157 | ("Commercial Contributor") hereby agrees to defend and 158 | indemnify every other Contributor ("Indemnified Contributor") 159 | against any losses, damages and costs (collectively "Losses") 160 | arising from claims, lawsuits and other legal actions brought by a third 161 | party against the Indemnified Contributor to the extent caused by the 162 | acts or omissions of such Commercial Contributor in connection with its 163 | distribution of the Program in a commercial product offering. The 164 | obligations in this section do not apply to any claims or Losses 165 | relating to any actual or alleged intellectual property infringement. In 166 | order to qualify, an Indemnified Contributor must: a) promptly notify 167 | the Commercial Contributor in writing of such claim, and b) allow the 168 | Commercial Contributor to control, and cooperate with the Commercial 169 | Contributor in, the defense and any related settlement negotiations. The 170 | Indemnified Contributor may participate in any such claim at its own 171 | expense.</p> 172 | 173 | <p>For example, a Contributor might include the Program in a commercial 174 | product offering, Product X. That Contributor is then a Commercial 175 | Contributor. If that Commercial Contributor then makes performance 176 | claims, or offers warranties related to Product X, those performance 177 | claims and warranties are such Commercial Contributor's responsibility 178 | alone. Under this section, the Commercial Contributor would have to 179 | defend claims against the other Contributors related to those 180 | performance claims and warranties, and if a court requires any other 181 | Contributor to pay any damages as a result, the Commercial Contributor 182 | must pay those damages.</p> 183 | 184 | <p><b>5. NO WARRANTY</b></p> 185 | 186 | <p>EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS 187 | PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 188 | OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, 189 | ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY 190 | OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely 191 | responsible for determining the appropriateness of using and 192 | distributing the Program and assumes all risks associated with its 193 | exercise of rights under this Agreement , including but not limited to 194 | the risks and costs of program errors, compliance with applicable laws, 195 | damage to or loss of data, programs or equipment, and unavailability or 196 | interruption of operations.</p> 197 | 198 | <p><b>6. DISCLAIMER OF LIABILITY</b></p> 199 | 200 | <p>EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT 201 | NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, 202 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING 203 | WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF 204 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 205 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR 206 | DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED 207 | HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.</p> 208 | 209 | <p><b>7. GENERAL</b></p> 210 | 211 | <p>If any provision of this Agreement is invalid or unenforceable under 212 | applicable law, it shall not affect the validity or enforceability of 213 | the remainder of the terms of this Agreement, and without further action 214 | by the parties hereto, such provision shall be reformed to the minimum 215 | extent necessary to make such provision valid and enforceable.</p> 216 | 217 | <p>If Recipient institutes patent litigation against any entity 218 | (including a cross-claim or counterclaim in a lawsuit) alleging that the 219 | Program itself (excluding combinations of the Program with other 220 | software or hardware) infringes such Recipient's patent(s), then such 221 | Recipient's rights granted under Section 2(b) shall terminate as of the 222 | date such litigation is filed.</p> 223 | 224 | <p>All Recipient's rights under this Agreement shall terminate if it 225 | fails to comply with any of the material terms or conditions of this 226 | Agreement and does not cure such failure in a reasonable period of time 227 | after becoming aware of such noncompliance. If all Recipient's rights 228 | under this Agreement terminate, Recipient agrees to cease use and 229 | distribution of the Program as soon as reasonably practicable. However, 230 | Recipient's obligations under this Agreement and any licenses granted by 231 | Recipient relating to the Program shall continue and survive.</p> 232 | 233 | <p>Everyone is permitted to copy and distribute copies of this 234 | Agreement, but in order to avoid inconsistency the Agreement is 235 | copyrighted and may only be modified in the following manner. The 236 | Agreement Steward reserves the right to publish new versions (including 237 | revisions) of this Agreement from time to time. No one other than the 238 | Agreement Steward has the right to modify this Agreement. The Eclipse 239 | Foundation is the initial Agreement Steward. The Eclipse Foundation may 240 | assign the responsibility to serve as the Agreement Steward to a 241 | suitable separate entity. Each new version of the Agreement will be 242 | given a distinguishing version number. The Program (including 243 | Contributions) may always be distributed subject to the version of the 244 | Agreement under which it was received. In addition, after a new version 245 | of the Agreement is published, Contributor may elect to distribute the 246 | Program (including its Contributions) under the new version. Except as 247 | expressly stated in Sections 2(a) and 2(b) above, Recipient receives no 248 | rights or licenses to the intellectual property of any Contributor under 249 | this Agreement, whether expressly, by implication, estoppel or 250 | otherwise. All rights in the Program not expressly granted under this 251 | Agreement are reserved.</p> 252 | 253 | <p>This Agreement is governed by the laws of the State of New York and 254 | the intellectual property laws of the United States of America. No party 255 | to this Agreement will bring a legal action under this Agreement more 256 | than one year after the cause of action arose. Each party waives its 257 | rights to a jury trial in any resulting litigation.</p> 258 | 259 | </body> 260 | 261 | </html> 262 | -------------------------------------------------------------------------------- /examples/animation/index.html: -------------------------------------------------------------------------------- 1 | <html> 2 | <body> 3 | <div id="app"></div> 4 | <script src="out/goog/base.js" type="text/javascript"></script> 5 | <script src="main.js" type="text/javascript"></script> 6 | <script type="text/javascript">goog.require("examples.animation.core");</script> 7 | </body> 8 | </html> 9 | -------------------------------------------------------------------------------- /examples/animation/src/core.cljs: -------------------------------------------------------------------------------- 1 | (ns examples.animation.core 2 | (:require [om.core :as om :include-macros true] 3 | [om.dom :as dom :include-macros true])) 4 | 5 | (enable-console-print!) 6 | 7 | (def app-state (atom {:count 0})) 8 | 9 | (defn animation-view [app owner] 10 | (reify 11 | om/IWillMount 12 | (will-mount [_] 13 | (js/setInterval 14 | (fn [] (om/transact! app :count inc)) 15 | 16)) 16 | om/IRender 17 | (render [_] 18 | (dom/div nil (:count app))))) 19 | 20 | (om/root animation-view app-state 21 | {:target (.getElementById js/document "app")}) 22 | -------------------------------------------------------------------------------- /examples/counters/index.html: -------------------------------------------------------------------------------- 1 | <html> 2 | <body> 3 | <div id="app0"></div> 4 | <div id="app1"></div> 5 | <script src="out/goog/base.js" type="text/javascript"></script> 6 | <script src="main.js" type="text/javascript"></script> 7 | <script type="text/javascript">goog.require("examples.counters.core");</script> 8 | </body> 9 | </html> 10 | -------------------------------------------------------------------------------- /examples/counters/src/core.cljs: -------------------------------------------------------------------------------- 1 | (ns examples.counters.core 2 | (:require-macros [cljs.core.async.macros :refer [go]]) 3 | (:require [om.core :as om :include-macros true] 4 | [om.dom :as dom :include-macros true] 5 | [cljs.core.async :refer [<! chan put! sliding-buffer]])) 6 | 7 | (enable-console-print!) 8 | 9 | (def app-state 10 | (atom {:counters (into [] (map (fn [n] {:id n :count 0}) (range 10)))})) 11 | 12 | (defn counter [data owner] 13 | (reify 14 | om/IRenderState 15 | (render-state [_ {:keys [last-clicked]}] 16 | (dom/div nil 17 | (dom/label nil (:count data)) 18 | (dom/button 19 | #js {:onClick 20 | (fn [e] 21 | (om/transact! data :count inc) 22 | (put! last-clicked (.-path data)))} 23 | "+") 24 | (dom/button 25 | #js {:onClick 26 | (fn [e] 27 | (om/transact! data :count dec) 28 | (put! last-clicked (.-path data)))} 29 | "-"))))) 30 | 31 | (defn counter-view [app owner] 32 | (reify 33 | om/IInitState 34 | (init-state [_] 35 | {:chans {:last-clicked (chan (sliding-buffer 1))}}) 36 | om/IWillMount 37 | (will-mount [_] 38 | (let [last-clicked (om/get-state owner [:chans :last-clicked])] 39 | (go (while true 40 | (let [lc (<! last-clicked)] 41 | (om/set-state! owner :message lc)))))) 42 | om/IRenderState 43 | (render-state [_ {:keys [message chans]}] 44 | (apply dom/div nil 45 | (dom/h1 #js {:key "head"} "A Counting Widget!") 46 | (dom/div 47 | #js {:key "message" 48 | :style 49 | (if message 50 | #js {:display "block"} 51 | #js {:display "none"})} 52 | (when message 53 | (str "Last clicked item was " (last message)))) 54 | (om/build-all counter (:counters app) 55 | {:key :id :init-state chans}))))) 56 | 57 | (om/root counter-view app-state 58 | {:target (.getElementById js/document "app0") 59 | :descriptor (om/no-local-descriptor om/no-local-state-methods) 60 | :tx-listen 61 | (fn [tx-data root-cursor] 62 | (println "listener 1: " tx-data))}) 63 | 64 | (om/root counter-view app-state 65 | {:target (.getElementById js/document "app1") 66 | :descriptor (om/no-local-descriptor om/no-local-state-methods) 67 | :tx-listen 68 | (fn [tx-data root-cursor] 69 | (println "listener 2: " tx-data))}) 70 | 71 | -------------------------------------------------------------------------------- /examples/cursor_as_key/index.html: -------------------------------------------------------------------------------- 1 | <html> 2 | <body> 3 | <div id="app"></div> 4 | <script src="out/goog/base.js" type="text/javascript"></script> 5 | <script src="main.js" type="text/javascript"></script> 6 | <script type="text/javascript">goog.require("examples.cursor_as_key.core");</script> 7 | </body> 8 | </html> 9 | -------------------------------------------------------------------------------- /examples/cursor_as_key/src/core.cljs: -------------------------------------------------------------------------------- 1 | (ns examples.cursor-as-key.core 2 | (:require [om.core :as om :include-macros true] 3 | [om.dom :as dom :include-macros true])) 4 | 5 | (enable-console-print!) 6 | 7 | (def app-state 8 | (atom 9 | {:selection [0 0] 10 | :values 11 | (into {} 12 | (for [x (range 64)] 13 | [[x x] (str "Hello " x)]))})) 14 | 15 | (defn app [app-state owner] 16 | (reify 17 | om/IRender 18 | (render [_] 19 | (let [selection (app-state :selection) 20 | values (get-in app-state [:values selection])] 21 | (dom/div nil values))))) 22 | 23 | (om/root app app-state 24 | {:target (.getElementById js/document "app")}) 25 | -------------------------------------------------------------------------------- /examples/harmful/index.html: -------------------------------------------------------------------------------- 1 | <html> 2 | <body> 3 | <div id="app"></div> 4 | <script src="out/goog/base.js" type="text/javascript"></script> 5 | <script src="main.js" type="text/javascript"></script> 6 | <script type="text/javascript">goog.require("examples.harmful.core");</script> 7 | </body> 8 | </html> 9 | -------------------------------------------------------------------------------- /examples/harmful/src/core.cljs: -------------------------------------------------------------------------------- 1 | (ns examples.harmful.core 2 | (:require [om.core :as om :include-macros true] 3 | [om.dom :as dom :include-macros true]) 4 | (:import [goog.ui IdGenerator])) 5 | 6 | (enable-console-print!) 7 | 8 | ;; ============================================================================= 9 | ;; Different backing React class 10 | 11 | (defn get-gstate [owner] 12 | (aget (.-props owner) "__om_app_state")) 13 | 14 | (defn merge-pending-state [owner] 15 | (let [gstate (get-gstate owner) 16 | spath [:state-map (om/id owner)] 17 | states (get-in @gstate spath)] 18 | (when (:pending-state states) 19 | (swap! gstate update-in spath 20 | (fn [states] 21 | (-> states 22 | (assoc :render-state 23 | (merge (:render-state states) (:pending-state states))) 24 | (dissoc :pending-state))))))) 25 | 26 | (def no-local-state-meths 27 | (assoc om/pure-methods 28 | :getInitialState 29 | (fn [] 30 | (this-as this 31 | (let [c (om/children this) 32 | props (.-props this) 33 | istate (or (aget props "__om_init_state") {}) 34 | id (or (::om/id istate) 35 | (.getNextUniqueId (.getInstance IdGenerator))) 36 | state (merge (dissoc istate ::om/id) 37 | (when (satisfies? om/IInitState c) 38 | (om/allow-reads (om/init-state c)))) 39 | spath [:state-map id :render-state]] 40 | (aset props "__om_init_state" nil) 41 | (swap! (get-gstate this) assoc-in spath state) 42 | #js {:__om_id id}))) 43 | :componentWillMount 44 | (fn [] 45 | (this-as this 46 | (om/merge-props-state this) 47 | (let [c (om/children this)] 48 | (when (satisfies? om/IWillMount c) 49 | (om/allow-reads (om/will-mount c)))) 50 | (merge-pending-state this))) 51 | :componentWillUnmount 52 | (fn [] 53 | (this-as this 54 | (let [c (om/children this) 55 | spath [:state-map (om/id this)]] 56 | (when (satisfies? om/IWillUnmount c) 57 | (om/allow-reads (om/will-unmount c))) 58 | (swap! (get-gstate this) update-in spath dissoc)))) 59 | :componentDidUpdate 60 | (fn [prev-props prev-state] 61 | (this-as this 62 | (let [c (om/children this) 63 | gstate (get-gstate this) 64 | states (get-in @gstate [:state-map (om/id this)]) 65 | spath [:state-map (om/id this)]] 66 | (when (satisfies? om/IDidUpdate c) 67 | (let [state (.-state this)] 68 | (om/allow-reads 69 | (om/did-update c 70 | (om/get-props #js {:props prev-props}) 71 | (or (:previous-state states) 72 | (:render-state states)))))) 73 | (when (:previous-state states) 74 | (swap! gstate update-in spath dissoc :previous-state))))))) 75 | 76 | (def no-local-descriptor 77 | (specify! (clj->js no-local-state-meths) 78 | om/ISetState 79 | (-set-state! 80 | ([this val render] 81 | (om/allow-reads 82 | (let [props (.-props this) 83 | app-state (aget props "__om_app_state") 84 | spath [:state-map (om/id this) :pending-state]] 85 | (swap! (get-gstate this) assoc-in spath val) 86 | (when (and (not (nil? app-state)) render) 87 | (om/-queue-render! app-state this))))) 88 | ([this ks val render] 89 | (om/allow-reads 90 | (let [props (.-props this) 91 | app-state (aget props "__om_app_state") 92 | spath [:state-map (om/id this) :pending-state]] 93 | (swap! (get-gstate this) update-in spath assoc-in ks val) 94 | (when (and (not (nil? app-state)) render) 95 | (om/-queue-render! app-state this)))))) 96 | om/IGetRenderState 97 | (-get-render-state 98 | ([this] 99 | (let [spath [:state-map (om/id this) :render-state]] 100 | (get-in @(get-gstate this) spath))) 101 | ([this ks] 102 | (get-in (om/-get-render-state this) ks))) 103 | om/IGetState 104 | (-get-state 105 | ([this] 106 | (let [spath [:state-map (om/id this)] 107 | states (get-in @(get-gstate this) spath)] 108 | (or (:pending-state states) 109 | (:render-state states)))) 110 | ([this ks] 111 | (get-in (om/-get-state this) ks))))) 112 | 113 | ;; ============================================================================= 114 | ;; Application 115 | 116 | (def app-state (atom {:title "A Counter!"})) 117 | 118 | (defn counter-view [data owner] 119 | (reify 120 | om/IInitState 121 | (init-state [_] 122 | {:count 0}) 123 | om/IRenderState 124 | (render-state [_ {:keys [count]}] 125 | (dom/div nil 126 | (dom/h2 nil (:title data)) 127 | (dom/div 128 | #js {:onClick (fn [e] (om/set-state! owner :count (inc count)))} 129 | count))))) 130 | 131 | (defn label-style [] 132 | #js {:style #js {:width "60px" :display "inline-block"}}) 133 | 134 | (defn debug-view [[f cursor opts] owner] 135 | (reify 136 | om/IInitState 137 | (init-state [_] 138 | {:id (.getNextUniqueId (.getInstance IdGenerator))}) 139 | om/IDidMount 140 | (did-mount [_] 141 | (om/update-state! owner :id identity)) 142 | om/IRenderState 143 | (render-state [_ {:keys [id]}] 144 | (dom/div nil 145 | (dom/div nil 146 | (dom/label (label-style) "Props:") 147 | (dom/code nil (pr-str (om/value cursor)))) 148 | (dom/div nil 149 | (dom/label (label-style) "State:") 150 | (dom/code nil 151 | (pr-str (get-in @(om/state cursor) [:state-map id :render-state])))) 152 | (om/build* f cursor 153 | (assoc opts :init-state {::om/id id})))))) 154 | 155 | (om/root 156 | (fn [app owner] 157 | (om/component 158 | (om/build counter-view app {:descriptor no-local-descriptor}))) 159 | app-state 160 | {:target (.getElementById js/document "app") 161 | :instrument 162 | (fn [f cursor opts] 163 | (if (= f counter-view) 164 | (om/build* debug-view [f cursor opts]) 165 | ::om/pass))}) 166 | 167 | -------------------------------------------------------------------------------- /examples/hello/index.html: -------------------------------------------------------------------------------- 1 | <html> 2 | <body> 3 | <div id="app"></div> 4 | <script src="out/goog/base.js" type="text/javascript"></script> 5 | <script src="main.js" type="text/javascript"></script> 6 | <script type="text/javascript">goog.require("examples.hello.core");</script> 7 | </body> 8 | </html> 9 | -------------------------------------------------------------------------------- /examples/hello/src/core.cljs: -------------------------------------------------------------------------------- 1 | (ns examples.hello.core 2 | (:require [om.core :as om :include-macros true] 3 | [om.dom :as dom :include-macros true])) 4 | 5 | (enable-console-print!) 6 | 7 | (def app-state (atom {:foo "bar"})) 8 | 9 | (defn widget [data owner] 10 | (reify 11 | om/ICheckState 12 | om/IInitState 13 | (init-state [_] 14 | {:count 0}) 15 | om/IWillMount 16 | (will-mount [_] 17 | (println "Hello widget mounting")) 18 | om/IWillUnmount 19 | (will-unmount [_] 20 | (println "Hello widget unmounting")) 21 | om/IRenderState 22 | (render-state [_ {:keys [count]}] 23 | (println "Render!") 24 | (dom/div nil 25 | (dom/h2 nil "Hello world!") 26 | (dom/p nil (str "Count: " count)) 27 | (dom/button 28 | #js {:onClick 29 | #(do 30 | (println "I can read!" (:foo data)) 31 | (om/update-state! owner :count inc))} 32 | "Bump!") 33 | (dom/button 34 | #js {:onClick 35 | #(do 36 | (println "I can also read!" (:foo data)) 37 | (om/update-state! owner :count identity))} 38 | "Do Nothing"))))) 39 | 40 | (om/root widget app-state 41 | {:target (.getElementById js/document "app")}) 42 | -------------------------------------------------------------------------------- /examples/input/index.html: -------------------------------------------------------------------------------- 1 | <html> 2 | <body> 3 | <div id="app"></div> 4 | <script src="out/goog/base.js" type="text/javascript"></script> 5 | <script src="main.js" type="text/javascript"></script> 6 | <script type="text/javascript">goog.require("examples.input.core");</script> 7 | </body> 8 | </html> 9 | -------------------------------------------------------------------------------- /examples/input/src/core.cljs: -------------------------------------------------------------------------------- 1 | (ns examples.input.core 2 | (:require [om.core :as om :include-macros true] 3 | [om.dom :as dom :include-macros true])) 4 | 5 | (enable-console-print!) 6 | 7 | (om/root 8 | (fn [app owner] 9 | (reify 10 | om/IInitState 11 | (init-state [_] 12 | {:value "" :count 0}) 13 | om/IRenderState 14 | (render-state [_ {:keys [value]}] 15 | (dom/div nil 16 | (dom/label nil "Only numeric : ") 17 | (dom/input #js 18 | {:value value 19 | :onChange 20 | #(let [new-value (-> % .-target .-value)] 21 | (if (js/isNaN new-value) 22 | (om/set-state! owner :value value) 23 | (om/set-state! owner :value new-value)))}))))) 24 | {} 25 | {:target (. js/document (getElementById "app"))}) 26 | 27 | -------------------------------------------------------------------------------- /examples/instrument/index.html: -------------------------------------------------------------------------------- 1 | <html> 2 | <body> 3 | <div id="app"></div> 4 | <script src="out/goog/base.js" type="text/javascript"></script> 5 | <script src="main.js" type="text/javascript"></script> 6 | <script type="text/javascript">goog.require("examples.instrument.core");</script> 7 | </body> 8 | </html> 9 | -------------------------------------------------------------------------------- /examples/instrument/src/core.cljs: -------------------------------------------------------------------------------- 1 | (ns examples.instrument.core 2 | (:require [om.core :as om :include-macros true] 3 | [om.dom :as dom :include-macros true])) 4 | 5 | (enable-console-print!) 6 | 7 | (defn sub-view [item owner] 8 | (reify 9 | om/IRender 10 | (render [_] 11 | (dom/div nil (:text item))))) 12 | 13 | (defn app-view [app owner] 14 | (reify 15 | om/IRender 16 | (render [_] 17 | (dom/div nil 18 | (dom/div nil (:text app)) 19 | (apply dom/ul nil 20 | (om/build-all sub-view (:list app))))))) 21 | 22 | (defn something-else [original owner opts] 23 | (reify 24 | om/IRender 25 | (render [_] 26 | (dom/div #js {:style #js {:border "1px solid #ccc" 27 | :padding "5px"}} 28 | (dom/div nil 29 | (dom/span nil "Path:") 30 | (dom/pre #js {:style #js {:display "inline-block"}} 31 | (pr-str (om/path (second original))))) 32 | (apply om/build* original))))) 33 | 34 | (om/root 35 | app-view 36 | (atom {:text "Instrument!" 37 | :list [{:text "Milk"} {:text "Cookies"} {:text "Apples"}]}) 38 | {:target (.getElementById js/document "app") 39 | :instrument 40 | (fn [f cursor m] 41 | (if (= f sub-view) 42 | (om/build* something-else [f cursor m]) 43 | ::om/pass))}) 44 | -------------------------------------------------------------------------------- /examples/mixins/index.html: -------------------------------------------------------------------------------- 1 | <html> 2 | <body> 3 | <div id="app"></div> 4 | <script src="out/goog/base.js" type="text/javascript"></script> 5 | <script src="main.js" type="text/javascript"></script> 6 | <script type="text/javascript">goog.require("examples.mixins.core");</script> 7 | </body> 8 | </html> 9 | -------------------------------------------------------------------------------- /examples/mixins/src/core.cljs: -------------------------------------------------------------------------------- 1 | (ns examples.mixins.core 2 | (:require [om.core :as om :include-macros true] 3 | [om.dom :as dom :include-macros true]) 4 | (:import [goog.ui IdGenerator])) 5 | 6 | (enable-console-print!) 7 | 8 | (def TestMixin 9 | #js {:componentWillMount 10 | (fn [] 11 | (println "TextMixin componentWillMount"))}) 12 | 13 | (def MyComponent 14 | (let [obj (om/specify-state-methods! (clj->js om/pure-methods))] 15 | (aset obj "mixins" #js [TestMixin]) 16 | (js/React.createClass obj))) 17 | 18 | (om/root 19 | (fn [app owner] 20 | (om/component 21 | (MyComponent. nil 22 | (dom/div nil "Hello world!")))) 23 | {} 24 | {:target (.getElementById js/document "app")}) 25 | -------------------------------------------------------------------------------- /examples/mouse/index.html: -------------------------------------------------------------------------------- 1 | <html> 2 | <body> 3 | <div id="app"></div> 4 | <script src="out/goog/base.js" type="text/javascript"></script> 5 | <script src="main.js" type="text/javascript"></script> 6 | <script type="text/javascript">goog.require("examples.mouse.core");</script> 7 | </body> 8 | </html> 9 | -------------------------------------------------------------------------------- /examples/mouse/src/core.cljs: -------------------------------------------------------------------------------- 1 | (ns examples.mouse.core 2 | (:require-macros [cljs.core.async.macros :refer [go]]) 3 | (:require [goog.events :as events] 4 | [cljs.core.async :as async :refer [>! <! put! chan]] 5 | [om.core :as om :include-macros true] 6 | [om.dom :as dom :include-macros true]) 7 | (:import [goog.events EventType])) 8 | 9 | (enable-console-print!) 10 | 11 | (defn listen [el type] 12 | (let [out (chan)] 13 | (events/listen el type #(put! out %)) 14 | out)) 15 | 16 | (defn mouse-view [app owner] 17 | (reify 18 | om/IWillMount 19 | (will-mount [_] 20 | (let [mouse-chan 21 | (async/map 22 | (fn [e] [(.-clientX e) (.-clientY e)]) 23 | [(listen js/window EventType/MOUSEMOVE)])] 24 | (go (while true 25 | (om/update! app :mouse (<! mouse-chan)))))) 26 | om/IRender 27 | (render [_] 28 | (dom/p nil 29 | (when-let [pos (:mouse app)] 30 | (pr-str (:mouse app))))))) 31 | 32 | (om/root mouse-view {:mouse nil} {:target (.getElementById js/document "app")}) 33 | -------------------------------------------------------------------------------- /examples/multi/index.html: -------------------------------------------------------------------------------- 1 | <html> 2 | <body> 3 | <div id="app"></div> 4 | <script src="out/goog/base.js" type="text/javascript"></script> 5 | <script src="main.js" type="text/javascript"></script> 6 | <script type="text/javascript">goog.require("examples.multi.core");</script> 7 | </body> 8 | </html> 9 | -------------------------------------------------------------------------------- /examples/multi/src/core.cljs: -------------------------------------------------------------------------------- 1 | (ns examples.multi.core 2 | (:require [om.core :as om :include-macros true] 3 | [om.dom :as dom :include-macros true])) 4 | 5 | (enable-console-print!) 6 | 7 | (def app-state 8 | (atom 9 | {:widgets 10 | [{:my-number 16} 11 | {:my-number 23}]})) 12 | 13 | (defmulti even-odd-widget 14 | (fn [props _] (even? (:my-number props)))) 15 | 16 | (defmethod even-odd-widget true 17 | [props owner] 18 | (reify 19 | om/IWillMount 20 | (will-mount [_] 21 | (println "Even widget mounting")) 22 | om/IWillUnmount 23 | (will-unmount [_] 24 | (println "Even widget unmounting")) 25 | om/IRender 26 | (render [_] 27 | (dom/div nil 28 | (dom/h2 nil "Even Widget: " (:my-number props)) 29 | (dom/p nil (:text props)) 30 | (dom/button 31 | #js {:onClick #(om/transact! props :my-number inc)} 32 | "+"))))) 33 | 34 | (defmethod even-odd-widget false 35 | [props owner] 36 | (reify 37 | om/IWillMount 38 | (will-mount [_] 39 | (println "Odd widget mounting")) 40 | om/IWillUnmount 41 | (will-unmount [_] 42 | (println "Odd widget unmounting")) 43 | om/IRender 44 | (render [_] 45 | (dom/div nil 46 | (dom/h2 nil (str "Odd Widget: " (:my-number props))) 47 | (dom/p nil (:text props)) 48 | (dom/button 49 | #js {:onClick #(om/transact! props :my-number inc)} 50 | "+"))))) 51 | 52 | (defn app [props owner] 53 | (reify 54 | om/IRender 55 | (render [_] 56 | (apply dom/div nil 57 | (om/build-all even-odd-widget (:widgets props)))))) 58 | 59 | (om/root app app-state 60 | {:target (.getElementById js/document "app")}) 61 | -------------------------------------------------------------------------------- /examples/multiroot/index.html: -------------------------------------------------------------------------------- 1 | <html> 2 | <body> 3 | <div id="app1"></div> 4 | <div id="app2"></div> 5 | <div id="modal1"></div> 6 | <div id="modal2"></div> 7 | <script src="out/goog/base.js" type="text/javascript"></script> 8 | <script src="main.js" type="text/javascript"></script> 9 | <script type="text/javascript">goog.require("examples.multiroot.core");</script> 10 | </body> 11 | </html> 12 | -------------------------------------------------------------------------------- /examples/multiroot/src/core.cljs: -------------------------------------------------------------------------------- 1 | (ns examples.multiroot.core 2 | (:require [om.core :as om :include-macros true] 3 | [om.dom :as dom :include-macros true])) 4 | 5 | (enable-console-print!) 6 | 7 | (def app-state 8 | (atom {:text "Hello!" 9 | :modal1 {:text "Modal 1"} 10 | :modal2 {:text "Modal 2"}})) 11 | 12 | (defn display [data owner] 13 | (om/component 14 | (dom/div nil (:text data)))) 15 | 16 | (om/root display app-state {:target (.getElementById js/document "app1")}) 17 | (om/root display app-state {:target (.getElementById js/document "app2")}) 18 | 19 | (swap! app-state assoc :text "Goodbye!") 20 | 21 | (defn modal [data owner] 22 | (om/component 23 | (dom/div nil (:text data)))) 24 | 25 | (om/root modal app-state 26 | {:target (.getElementById js/document "modal1") 27 | :path [:modal1]}) 28 | 29 | (om/root modal app-state 30 | {:target (.getElementById js/document "modal2") 31 | :path [:modal2]}) 32 | -------------------------------------------------------------------------------- /examples/refs/index.html: -------------------------------------------------------------------------------- 1 | <html> 2 | <body> 3 | <div id="app"></div> 4 | <script src="out/goog/base.js" type="text/javascript"></script> 5 | <script src="main.js" type="text/javascript"></script> 6 | <script type="text/javascript">goog.require("examples.refs.core");</script> 7 | </body> 8 | </html> 9 | -------------------------------------------------------------------------------- /examples/refs/src/core.cljs: -------------------------------------------------------------------------------- 1 | (ns examples.refs.core 2 | (:require [om.core :as om :include-macros true] 3 | [om.dom :as dom :include-macros true])) 4 | 5 | (enable-console-print!) 6 | 7 | (def app-state 8 | (atom {:items [{:text "cat"} 9 | {:text "dog"} 10 | {:text "bird"}]})) 11 | 12 | (def app-history (atom [@app-state])) 13 | 14 | (defn items [] 15 | (om/ref-cursor (:items (om/root-cursor app-state)))) 16 | 17 | (defn aview [{:keys [text]} owner] 18 | (reify 19 | om/IRender 20 | (render [_] 21 | (println "Render" text) 22 | (let [xs (om/observe owner (items))] 23 | (dom/div nil 24 | (dom/h2 nil text) 25 | (apply dom/ul nil 26 | (map #(dom/li nil (:text %)) xs))))))) 27 | 28 | (defn main-view [_ owner] 29 | (reify 30 | om/IRender 31 | (render [_] 32 | (println "Render Main View") 33 | (let [xs (items)] 34 | (dom/div nil 35 | (om/build aview {:text "View A"}) 36 | (om/build aview {:text "View B"}) 37 | (dom/button 38 | #js {:onClick 39 | (fn [e] (om/transact! xs #(assoc % 1 {:text "zebra"})))} 40 | "Switch To Zebra!") 41 | (dom/button 42 | #js {:onClick 43 | (fn [e] (om/transact! xs #(assoc % 1 {:text "dog"})))} 44 | "Switch to Dog!")))))) 45 | 46 | (defn root [empty owner] 47 | (reify 48 | om/IRender 49 | (render [_] 50 | (println "Render Root") 51 | (dom/div nil 52 | (om/build main-view {}) 53 | (dom/div #js {:id "message"} nil) 54 | (dom/button 55 | #js {:onClick 56 | (fn [e] 57 | (when (> (count @app-history) 1) 58 | (swap! app-history pop) 59 | (reset! app-state (last @app-history))))} 60 | "Undo"))))) 61 | 62 | (om/root root app-state 63 | {:target (.getElementById js/document "app")}) 64 | 65 | (defn pluralize [n s] 66 | (if (== n 1) 67 | s 68 | (str s "s"))) 69 | 70 | (add-watch app-state :history 71 | (fn [_ _ _ n] 72 | (when-not (= (last @app-history) n) 73 | (swap! app-history conj n)) 74 | (set! (.-innerHTML (.getElementById js/document "message")) 75 | (let [c (count @app-history)] 76 | (str c " Saved " (pluralize c "State")))))) 77 | -------------------------------------------------------------------------------- /examples/shared/index.html: -------------------------------------------------------------------------------- 1 | <html> 2 | <body> 3 | <div id="app"></div> 4 | <script src="out/goog/base.js" type="text/javascript"></script> 5 | <script src="main.js" type="text/javascript"></script> 6 | <script type="text/javascript">goog.require("examples.shared.core");</script> 7 | </body> 8 | </html> 9 | -------------------------------------------------------------------------------- /examples/shared/src/core.cljs: -------------------------------------------------------------------------------- 1 | (ns examples.shared.core 2 | (:require [om.core :as om :include-macros true] 3 | [om.dom :as dom :include-macros true])) 4 | 5 | (enable-console-print!) 6 | 7 | (def app-state (atom {:title "Shared example"})) 8 | 9 | (defn child-view [data owner] 10 | (reify 11 | om/IRender 12 | (render [_] 13 | (dom/div nil 14 | (om/get-shared owner :some-text))))) 15 | 16 | (defn shared-view [app owner] 17 | (reify 18 | om/IRender 19 | (render [_] 20 | (dom/div nil 21 | (dom/h1 nil (:title app)) 22 | (dom/p nil 23 | (om/get-shared owner :some-text)) 24 | (om/build child-view {}))))) 25 | 26 | (om/root shared-view app-state 27 | {:target (.getElementById js/document "app") 28 | :shared {:some-text "I'm shared!"}}) 29 | -------------------------------------------------------------------------------- /examples/sortable/index.html: -------------------------------------------------------------------------------- 1 | <html> 2 | <head> 3 | <style> 4 | #app { 5 | width: 300px; 6 | } 7 | .sortable { 8 | width: 300px; 9 | } 10 | ul { 11 | border-top: 1px solid #ccc; 12 | border-left: 1px solid #ccc; 13 | border-right: 1px solid #ccc; 14 | margin: 0; 15 | padding: 0; 16 | } 17 | li { 18 | background-color: white; 19 | padding: 4px; 20 | list-style: none; 21 | border-bottom: 1px solid #ccc; 22 | user-select: none; 23 | -moz-user-select: none; 24 | -webkit-user-select: none; 25 | cursor: move; 26 | box-sizing: border-box; 27 | -moz-box-sizing: border-box; 28 | -webkit-box-sizing: border-box; 29 | } 30 | li.dragging { 31 | border: 1px solid #ccc; 32 | } 33 | p { 34 | padding: 0; 35 | margin: 0; 36 | } 37 | </style> 38 | </head> 39 | <body> 40 | <div id="app"></div> 41 | <script src="out/goog/base.js" type="text/javascript"></script> 42 | <script src="main.js" type="text/javascript"></script> 43 | <script type="text/javascript">goog.require("examples.sortable.core");</script> 44 | </body> 45 | </html> 46 | -------------------------------------------------------------------------------- /examples/sortable/src/core.cljs: -------------------------------------------------------------------------------- 1 | (ns examples.sortable.core 2 | (:require-macros [cljs.core.async.macros :refer [go alt!]]) 3 | (:require [cljs.core.async :as async :refer [put! chan dropping-buffer]] 4 | [om.core :as om :include-macros true] 5 | [om.dom :as dom :include-macros true] 6 | [goog.events :as events] 7 | [goog.style :as gstyle]) 8 | (:import [goog.ui IdGenerator] 9 | [goog.events EventType])) 10 | 11 | (enable-console-print!) 12 | 13 | ;; ============================================================================= 14 | ;; Utilities 15 | 16 | (defn guid [] 17 | (.getNextUniqueId (.getInstance IdGenerator))) 18 | 19 | (defn gsize->vec [size] 20 | [(.-width size) (.-height size)]) 21 | 22 | (defn to? [owner next-props next-state k] 23 | (or (and (not (om/get-render-state owner k)) 24 | (k next-state)) 25 | (and (not (k (om/get-props owner))) 26 | (k next-props)))) 27 | 28 | (defn from? [owner next-props next-state k] 29 | (or (and (om/get-render-state owner k) 30 | (not (k next-state))) 31 | (and (k (om/get-props owner)) 32 | (not (k next-props))))) 33 | 34 | (defn location [e] 35 | [(.-clientX e) (.-clientY e)]) 36 | 37 | (defn element-offset [el] 38 | (let [offset (gstyle/getPageOffset el)] 39 | [(.-x offset) (.-y offset)])) 40 | 41 | ;; ============================================================================= 42 | ;; Generic Draggable 43 | 44 | (defn dragging? [owner] 45 | (om/get-state owner :dragging)) 46 | 47 | (defn drag-start [e item owner] 48 | (when-not (dragging? owner) 49 | (let [el (om/get-node owner "draggable") 50 | state (om/get-state owner) 51 | drag-start (location e) 52 | el-offset (element-offset el) 53 | drag-offset (vec (map - el-offset drag-start))] 54 | ;; if in a sortable need to wait for sortable to 55 | ;; initiate dragging 56 | (when-not (:delegate state) 57 | (om/set-state! owner :dragging true)) 58 | (doto owner 59 | (om/set-state! :location 60 | ((or (:constrain state) identity) el-offset)) 61 | (om/set-state! :drag-offset drag-offset)) 62 | (when-let [c (:chan state)] 63 | (put! c 64 | {:event :drag-start 65 | :id (:id item) 66 | :location (vec (map + drag-start drag-offset))}))))) 67 | 68 | (defn drag-stop [e item owner] 69 | (when (dragging? owner) 70 | (let [state (om/get-state owner)] 71 | (when (:dragging state) 72 | (om/set-state! owner :dragging false)) 73 | ;; rendering order issues otherwise 74 | (when-not (:delegate state) 75 | (doto owner 76 | (om/set-state! :location nil) 77 | (om/set-state! :drag-offset nil))) 78 | (when-let [c (:chan state)] 79 | (put! c {:event :drag-stop :id (:id item)}))))) 80 | 81 | (defn drag [e item owner] 82 | (let [state (om/get-state owner)] 83 | (when (dragging? owner) 84 | (let [loc ((or (:constrain state) identity) 85 | (vec (map + (location e) (:drag-offset state))))] 86 | (om/set-state! owner :location loc) 87 | (when-let [c (:chan state)] 88 | (put! c {:event :drag :location loc :id (:id item)})))))) 89 | 90 | (defn draggable [item owner] 91 | (reify 92 | om/IDidMount 93 | (did-mount [_] 94 | ;; capture the cell dimensions when it becomes available 95 | (let [dims (-> (om/get-node owner "draggable") 96 | gstyle/getSize gsize->vec)] 97 | (om/set-state! owner :dimensions dims) 98 | ;; let cell dimension listeners know 99 | (when-let [dims-chan (:dims-chan (om/get-state owner))] 100 | (put! dims-chan dims)))) 101 | om/IWillUpdate 102 | (will-update [_ next-props next-state] 103 | ;; begin dragging, need to track events on window 104 | (when (or (to? owner next-props next-state :dragging)) 105 | (let [mouse-up #(drag-stop % @next-props owner) 106 | mouse-move #(drag % @next-props owner)] 107 | (om/set-state! owner :window-listeners 108 | [mouse-up mouse-move]) 109 | (doto js/window 110 | (events/listen EventType.MOUSEUP mouse-up) 111 | (events/listen EventType.MOUSEMOVE mouse-move)))) 112 | ;; end dragging, cleanup window event listeners 113 | (when (from? owner next-props next-state :dragging) 114 | (let [[mouse-up mouse-move] 115 | (om/get-state owner :window-listeners)] 116 | (doto js/window 117 | (events/unlisten EventType.MOUSEUP mouse-up) 118 | (events/unlisten EventType.MOUSEMOVE mouse-move))))) 119 | om/IRenderState 120 | (render-state [_ state] 121 | (let [style (cond 122 | (dragging? owner) 123 | (let [[x y] (:location state) 124 | [w h] (:dimensions state)] 125 | #js {:position "absolute" 126 | :top y :left x :z-index 1 127 | :width w :height h}) 128 | :else 129 | #js {:position "static" :z-index 0})] 130 | (dom/li 131 | #js {:className (when (dragging? owner) "dragging") 132 | :style style 133 | :ref "draggable" 134 | :onMouseDown #(drag-start % @item owner) 135 | :onMouseUp #(drag-stop % @item owner) 136 | :onMouseMove #(drag % @item owner)} 137 | (om/build (:view state) item)))))) 138 | 139 | ;; ============================================================================= 140 | ;; Generic Sortable 141 | 142 | (defn from-loc [v1 v2] 143 | (vec (map - v2 v1))) 144 | 145 | (defn sortable-spacer [height] 146 | (dom/li 147 | #js {:key "spacer-cell" 148 | :style #js {:height height}})) 149 | 150 | (defn index-of [x v] 151 | (loop [i 0 v (seq v)] 152 | (if v 153 | (if (= x (first v)) 154 | i 155 | (recur (inc i) (next v))) 156 | -1))) 157 | 158 | (defn insert-at [x idx ignore v] 159 | (let [len (count v)] 160 | (loop [i 0 v v ret []] 161 | (if (>= i len) 162 | (conj ret x) 163 | (let [y (first v)] 164 | (if (= y ignore) 165 | (recur i (next v) (conj ret y)) 166 | (if (== i idx) 167 | (into (conj ret x) v) 168 | (recur (inc i) (next v) (conj ret y))))))))) 169 | 170 | (defn sorting? [owner] 171 | (om/get-state owner :sorting)) 172 | 173 | (defn start-sort [owner e] 174 | (let [state (om/get-state owner) 175 | sort (:sort state) 176 | idx (index-of (:id e) sort)] 177 | (doto owner 178 | (om/set-state! :sorting (:id e)) 179 | (om/set-state! :real-sort sort) 180 | (om/set-state! :drop-index idx) 181 | (om/set-state! :sort (insert-at ::spacer idx (:id e) sort))))) 182 | 183 | (defn handle-drop [owner e] 184 | (when (sorting? owner) 185 | (let [{:keys [sort drop-index]} (om/get-state owner) 186 | idx (index-of ::spacer sort) 187 | sort (->> sort 188 | (remove #{(:id e)}) 189 | (replace {::spacer (:id e)}) 190 | vec)] 191 | (doto owner 192 | (om/set-state! :sorting nil) 193 | (om/set-state! :drop-index nil) 194 | (om/set-state! :real-sort nil) 195 | (om/set-state! :sort sort))))) 196 | 197 | (defn update-drop [owner e] 198 | (when (sorting? owner) 199 | (let [loc (:location e) 200 | state (om/get-state owner) 201 | [_ y] (from-loc (:location state) loc) 202 | [_ ch] (:cell-dimensions state) 203 | drop-index (js/Math.round (/ y ch))] 204 | (when (not= (:drop-index state) drop-index) 205 | (doto owner 206 | (om/set-state! :drop-index drop-index) 207 | (om/set-state! :sort 208 | (insert-at ::spacer drop-index (:id e) (:real-sort state)))))))) 209 | 210 | (defn bound [n lb ub] 211 | (cond 212 | (< n lb) lb 213 | (> n ub) ub 214 | :else n)) 215 | 216 | (defn handle-drag-event [owner e] 217 | (case (:event e) 218 | :drag-start (start-sort owner e) 219 | :drag-stop (handle-drop owner e) 220 | :drag (update-drop owner e) 221 | nil)) 222 | 223 | (defn sortable [{:keys [items sort]} owner] 224 | (reify 225 | om/IInitState 226 | (init-state [_] {:sort (om/value sort)}) 227 | om/IWillMount 228 | (will-mount [_] 229 | (let [drag-chan (chan) 230 | dims-chan (chan (dropping-buffer 1))] 231 | (om/set-state! owner :chans 232 | {:drag-chan drag-chan 233 | :dims-chan dims-chan}) 234 | (go 235 | (while true 236 | (alt! 237 | drag-chan ([e c] (handle-drag-event owner e)) 238 | dims-chan ([e c] (om/set-state! owner :cell-dimensions e))))))) 239 | ;; doesn't account for change in number of items in sortable 240 | ;; nor changes in sort - exercise for the reader 241 | om/IWillUpdate 242 | (will-update [_ next-props next-state] 243 | ;; calculate constraints from cell-dimensions when we receive them 244 | (when (to? owner next-props next-state :cell-dimensions) 245 | (let [node (om/get-node owner "sortable") 246 | [w h] (gsize->vec (gstyle/getSize node)) 247 | [x y] (element-offset node) 248 | [_ ch] (:cell-dimensions next-state)] 249 | (om/set-state! owner :constrain 250 | (fn [[_ cy]] [(inc x) (bound cy y (- (+ y h) ch))]))))) 251 | om/IDidMount 252 | (did-mount [_] 253 | (when-not (om/get-state owner :location) 254 | (om/set-state! owner :location 255 | (element-offset (om/get-node owner "sortable"))))) 256 | om/IRenderState 257 | (render-state [_ state] 258 | (apply dom/ul #js {:className "sortable" :ref "sortable"} 259 | (map 260 | (fn [id] 261 | (if-not (= id ::spacer) 262 | (om/build draggable (items id) 263 | (let [{:keys [constrain chans view]} state] 264 | {:key :id 265 | :init-state {:view view 266 | :chan (:drag-chan chans) 267 | :dims-chan (:dims-chan chans) 268 | :delegate true} 269 | :state {:constrain constrain 270 | :dragging (= id (:sorting state))}})) 271 | (sortable-spacer (second (:cell-dimensions state))))) 272 | (:sort state)))))) 273 | 274 | ;; ============================================================================= 275 | ;; Example 276 | 277 | (def app-state 278 | (let [items (->> (take 10 (map vector (repeatedly guid) (range))) 279 | (map (fn [[id n]] [id {:id id :title n}])) 280 | (into {}))] 281 | (atom {:items items 282 | :sort (into [] (keys items))}))) 283 | 284 | (defn item [the-item owner] 285 | (om/component (dom/span nil (str "Item " (:title the-item))))) 286 | 287 | (defn sortable-view [app owner] 288 | (om/component 289 | (dom/div nil 290 | (dom/h2 nil "Sortable example") 291 | (om/build sortable app {:init-state {:view item}})))) 292 | 293 | (om/root sortable-view app-state 294 | {:target (.getElementById js/document "app")}) 295 | -------------------------------------------------------------------------------- /examples/state_bug/index.html: -------------------------------------------------------------------------------- 1 | <html> 2 | <body> 3 | <div id="app"></div> 4 | <script src="main.js" type="text/javascript"></script> 5 | </body> 6 | </html> 7 | -------------------------------------------------------------------------------- /examples/state_bug/src/core.cljs: -------------------------------------------------------------------------------- 1 | (ns examples.state-bug.core 2 | (:require [om.core :as om :include-macros true] 3 | [om.dom :as dom :include-macros true])) 4 | 5 | (enable-console-print!) 6 | 7 | (def app-state (atom {})) 8 | 9 | (defn child-view [_ _ {:keys [inform!]}] 10 | (reify 11 | om/IRender 12 | (render [_] 13 | (inform!) 14 | (dom/div nil "Child")))) 15 | 16 | (defn parent-view [_ owner] 17 | (reify 18 | om/IRenderState 19 | (render-state 20 | [_ {:keys [foo] :as state}] 21 | (dom/div nil 22 | (str "Foo is: " (pr-str foo)) 23 | (dom/div nil 24 | (om/build child-view 25 | nil 26 | {:opts {:inform! (fn [] (om/set-state! owner :foo "bar"))}})))))) 27 | 28 | (defn application [data owner] 29 | (reify 30 | om/IInitState 31 | (init-state [_] 32 | {:cfg-init false}) 33 | om/IWillMount 34 | (will-mount [_] 35 | (js/setTimeout #(om/set-state! owner :cfg-init true) 500)) 36 | om/IRenderState 37 | (render-state [_ {:keys [cfg-init]}] 38 | ;; this works fine 39 | #_(om/build parent-view {}) 40 | ;; start problem code 41 | ;; comment out this code and uncomment the above to see it work 42 | (if cfg-init 43 | (om/build parent-view {}) 44 | (dom/div nil)) 45 | ;; end problem code 46 | ))) 47 | 48 | (defn start [dom-id] 49 | (om/root application app-state 50 | {:target (.getElementById js/document dom-id)})) 51 | 52 | (start "app") 53 | -------------------------------------------------------------------------------- /examples/stateful/index.html: -------------------------------------------------------------------------------- 1 | <html> 2 | <body> 3 | <div id="app"></div> 4 | <script src="out/goog/base.js" type="text/javascript"></script> 5 | <script src="main.js" type="text/javascript"></script> 6 | <script type="text/javascript">goog.require("examples.stateful.core");</script> 7 | </body> 8 | </html> 9 | -------------------------------------------------------------------------------- /examples/stateful/src/core.cljs: -------------------------------------------------------------------------------- 1 | (ns examples.stateful.core 2 | (:require [om.core :as om :include-macros true] 3 | [om.dom :as dom :include-macros true])) 4 | 5 | (defn counter-view [data owner] 6 | (reify 7 | om/IRender 8 | (render [_] 9 | (dom/div nil 10 | (dom/h2 nil "A Counter!") 11 | (dom/div 12 | #js {:onClick (fn [e] (om/transact! data :count inc))} 13 | (:count data)))))) 14 | 15 | (defn applyf [x korks f] 16 | (if (empty? korks) 17 | (f x) 18 | (update-in x korks f))) 19 | 20 | (def MyClass* 21 | (js/React.createClass 22 | #js 23 | {:getInitialState 24 | (fn [] #js {:value {:count 0}}) 25 | :render 26 | (fn [] 27 | (this-as this 28 | (let [counter (aget (.-state this) "value")] 29 | (om/build counter-view 30 | (specify! counter 31 | IDeref 32 | (-deref [this] this) 33 | om/ITransact 34 | (-transact! [m korks f _] 35 | (.setState this #js {:value (applyf m korks f)})))))))})) 36 | 37 | (def MyClass (js/React.createFactory MyClass*)) 38 | 39 | (js/React.renderComponent (MyClass.) (.getElementById js/document "app")) 40 | -------------------------------------------------------------------------------- /examples/two_lists/index.html: -------------------------------------------------------------------------------- 1 | <html> 2 | <body> 3 | <div id="app"></div> 4 | <script src="out/goog/base.js" type="text/javascript"></script> 5 | <script src="main.js" type="text/javascript"></script> 6 | <script type="text/javascript">goog.require("examples.two_lists.core");</script> 7 | </body> 8 | </html> 9 | -------------------------------------------------------------------------------- /examples/two_lists/src/core.cljs: -------------------------------------------------------------------------------- 1 | (ns examples.two-lists.core 2 | (:require [om.core :as om :include-macros true] 3 | [om.dom :as dom :include-macros true])) 4 | 5 | (enable-console-print!) 6 | 7 | (defprotocol IResolve 8 | (-resolve [this id])) 9 | 10 | (def app-state 11 | (atom 12 | {:list0 [:a :b :c] 13 | :list1 [:d :e :f] 14 | :sublist0 [:g :h :i] 15 | :items 16 | {:a {:text "cat"} 17 | :b {:text "dog"} 18 | :c {:text "bird"} 19 | :d {:text "lion"} 20 | :e {:text "antelope"} 21 | :f {:text "zebra"} 22 | :g {:text "anteater"} 23 | :h {:text "hyena"} 24 | :i {:text "tiger"}}})) 25 | 26 | (defn removable [item id order] 27 | (specify item 28 | Object 29 | (remove 30 | ([this x] 31 | (om/transact! order #(vec (remove #{id} %))))))) 32 | 33 | (defn collection [order data] 34 | (specify order 35 | ISeqable 36 | (-seq [_] 37 | (map #(removable (get data %) % order) order)) 38 | ILookup 39 | (-lookup 40 | ([this k] (-lookup this k nil)) 41 | ([this k not-found] 42 | (-nth this k not-found))) 43 | IIndexed 44 | (-nth 45 | ([this k] (-nth this k nil)) 46 | ([this k not-found] 47 | (let [id (nth order k)] 48 | (removable (get data id) id order)))))) 49 | 50 | (defn item-view [aitem owner] 51 | (reify 52 | om/IRender 53 | (render [_] 54 | (dom/li nil 55 | (dom/div nil (str (:text aitem))) 56 | (dom/button 57 | #js {:onClick #(.remove aitem)} 58 | "Remove!"))))) 59 | 60 | (defn list-view [alist owner] 61 | (reify 62 | om/IRender 63 | (render [_] 64 | (apply dom/ul nil (om/build-all item-view alist))))) 65 | 66 | (defn resolveable [x resolve-fn] 67 | (if (om/cursor? x) 68 | (specify x 69 | ICloneable 70 | (-clone [_] 71 | (resolveable (-clone x) resolve-fn)) 72 | IResolve 73 | (-resolve [this id] 74 | (resolve-fn id this)) 75 | om/ICursorDerive 76 | (-derive [this derived state path] 77 | (resolveable (om/to-cursor derived state path) resolve-fn))) 78 | x)) 79 | 80 | (defn resolve-id [id cursor] 81 | (let [state (om/state cursor)] 82 | (om/to-cursor (get @state id) [] state))) 83 | 84 | (defn my-app [global owner] 85 | (reify 86 | om/IRender 87 | (render [_] 88 | (let [global (resolveable global resolve-id) 89 | items (:items global)] 90 | (dom/div nil 91 | (dom/h2 nil "Two Lists") 92 | (om/build list-view (collection (:list0 global) items)) 93 | (om/build list-view (collection (:list1 global) items))))))) 94 | 95 | (om/root my-app app-state 96 | {:target (.getElementById js/document "app")}) 97 | -------------------------------------------------------------------------------- /examples/typeahead/index.html: -------------------------------------------------------------------------------- 1 | <html> 2 | <head> 3 | <style> 4 | ul { 5 | margin: 0; 6 | padding: 0; 7 | } 8 | li { 9 | list-style: none; 10 | } 11 | p { 12 | padding: 0; 13 | margin: 0; 14 | } 15 | </style> 16 | </head> 17 | <body> 18 | <div id="app"></div> 19 | <script src="out/goog/base.js" type="text/javascript"></script> 20 | <script src="main.js" type="text/javascript"></script> 21 | <script type="text/javascript">goog.require("examples.typeahead.core");</script> 22 | </body> 23 | </html> 24 | -------------------------------------------------------------------------------- /examples/typeahead/src/core.cljs: -------------------------------------------------------------------------------- 1 | (ns examples.typeahead.core 2 | (:refer-clojure :exclude [chars]) 3 | (:require [om.core :as om :include-macros true] 4 | [om.dom :as dom :include-macros true] 5 | [clojure.string :as string])) 6 | 7 | (enable-console-print!) 8 | 9 | (def chars (into [] "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz")) 10 | 11 | (defn rand-char [] 12 | (nth chars (rand-int (count chars)))) 13 | 14 | (defn rand-word [] 15 | (apply str (take (inc (rand-int 10)) (repeatedly rand-char)))) 16 | 17 | (def app-state 18 | (atom 19 | {:words 20 | (into [] 21 | (map (fn [w i] {:index i :word w :count (count w)}) 22 | (sort (into [] (take 200 (repeatedly rand-word)))) 23 | (range)))})) 24 | 25 | (extend-type string 26 | ICloneable 27 | (-clone [s] (js/String. s))) 28 | 29 | (extend-type js/String 30 | om/IValue 31 | (-value [s] (str s)) 32 | ICloneable 33 | (-clone [s] (js/String. s))) 34 | 35 | (extend-type number 36 | ICloneable 37 | (-clone [n] (js/Number. n))) 38 | 39 | (extend-type js/Number 40 | om/IValue 41 | (-value [n] (.valueOf n)) 42 | ICloneable 43 | (-clone [n] (js/Number. n))) 44 | 45 | (defn hidden [^boolean bool] 46 | (if bool 47 | #js {:display "none"} 48 | #js {:display "block"})) 49 | 50 | ;; we have to use om/value because we've made 51 | ;; strings and numbers work as cursors and React doesn't 52 | ;; know how to handle these correctly 53 | 54 | (defn word-index [index owner] 55 | (om/component (dom/span nil (om/value index)))) 56 | 57 | (defn word-count [count owner] 58 | (om/component (dom/span nil (om/value count)))) 59 | 60 | (defn word [the-word owner] 61 | (om/component (dom/span nil (om/value the-word)))) 62 | 63 | (defn item [the-item owner] 64 | (om/component 65 | (dom/li #js {:style (hidden (:hidden the-item))} 66 | (om/build word-index (:index the-item)) 67 | (dom/span nil " ") 68 | (om/build word (:word the-item)) 69 | (dom/span nil " ") 70 | (om/build word-count (:count the-item))))) 71 | 72 | (defn change [e owner] 73 | (om/set-state! owner :text (.. e -target -value))) 74 | 75 | (defn typeahead [data owner] 76 | (reify 77 | om/IInitState 78 | (init-state [_] {:text ""}) 79 | om/IRenderState 80 | (render-state [_ {:keys [text]}] 81 | (let [words (:words data)] 82 | (dom/div nil 83 | (dom/h2 nil "Type ahead example") 84 | (dom/input 85 | #js {:type "text" 86 | :ref "text-field" 87 | :value text 88 | :onChange #(change % owner)}) 89 | (apply dom/ul nil 90 | (om/build-all item words 91 | {:fn (fn [x] 92 | (if-not (string/blank? text) 93 | (cond-> x 94 | (not (zero? (.indexOf (:word x) text))) (assoc :hidden true)) 95 | x))}))))))) 96 | 97 | (om/root typeahead app-state {:target (.getElementById js/document "app")}) 98 | -------------------------------------------------------------------------------- /examples/unmount/index.html: -------------------------------------------------------------------------------- 1 | <html> 2 | <body> 3 | <div id="app"></div> 4 | <script src="out/goog/base.js" type="text/javascript"></script> 5 | <script src="main.js" type="text/javascript"></script> 6 | <script type="text/javascript">goog.require("examples.unmount.core");</script> 7 | </body> 8 | </html> 9 | -------------------------------------------------------------------------------- /examples/unmount/src/core.cljs: -------------------------------------------------------------------------------- 1 | (ns examples.unmount.core 2 | (:require [om.core :as om :include-macros true] 3 | [om.dom :as dom :include-macros true])) 4 | 5 | (enable-console-print!) 6 | 7 | (defn widget-a [data owner] 8 | (reify 9 | om/IWillUnmount 10 | (will-unmount [_] 11 | (println "umounting Widget A")) 12 | om/IRender 13 | (render [_] 14 | (dom/div nil "Widget A")))) 15 | 16 | (defn widget-b [data owner] 17 | (reify 18 | om/IWillUnmount 19 | (will-unmount [_] 20 | (println "umounting Widget B")) 21 | om/IRender 22 | (render [_] 23 | (dom/div nil "Widget B")))) 24 | 25 | (defn app [data owner] 26 | (reify 27 | om/IRender 28 | (render [_] 29 | (dom/div nil 30 | (if (= (:widget data) :a) 31 | (om/build widget-a {}) 32 | (om/build widget-b {})) 33 | (dom/button 34 | #js {:onClick (fn [e] (om/transact! data :widget {:a :b :b :a}))} 35 | "Switch!"))))) 36 | 37 | (om/root app {:widget :a} 38 | {:target (.getElementById js/document "app")}) 39 | -------------------------------------------------------------------------------- /examples/update_props/index.html: -------------------------------------------------------------------------------- 1 | <html> 2 | <body> 3 | <div id="app"></div> 4 | <script src="out/goog/base.js" type="text/javascript"></script> 5 | <script src="main.js" type="text/javascript"></script> 6 | <script type="text/javascript">goog.require("examples.update_props.core");</script> 7 | </body> 8 | </html> 9 | -------------------------------------------------------------------------------- /examples/update_props/src/core.cljs: -------------------------------------------------------------------------------- 1 | (ns examples.update-props.core 2 | (:require [om.core :as om :include-macros true] 3 | [om.dom :as dom :include-macros true])) 4 | 5 | (enable-console-print!) 6 | 7 | (def app-state (atom {:widget {:count 0}})) 8 | 9 | (defn widget [_ owner] 10 | (reify 11 | om/IRenderProps 12 | (render-props [_ props _] 13 | (println "Widget render!") 14 | (dom/div nil 15 | (dom/h2 nil "A Widget") 16 | (dom/p nil (str "Count: " (:count props))) 17 | (dom/button #js 18 | {:onClick #(om/update-props! owner props [:count] inc)} 19 | "+"))))) 20 | 21 | (defn my-app [global owner] 22 | (reify 23 | om/IRender 24 | (render [_] 25 | (println "Root render!") 26 | (dom/div nil 27 | (om/build widget (:widget global)))))) 28 | 29 | (om/root my-app app-state 30 | {:target (.getElementById js/document "app")}) 31 | -------------------------------------------------------------------------------- /examples/verify/index.html: -------------------------------------------------------------------------------- 1 | <html> 2 | <body> 3 | <div id="app"></div> 4 | <script src="out/goog/base.js" type="text/javascript"></script> 5 | <script src="main.js" type="text/javascript"></script> 6 | <script type="text/javascript">goog.require("examples.verify.core");</script> 7 | </body> 8 | </html> 9 | -------------------------------------------------------------------------------- /examples/verify/src/core.cljs: -------------------------------------------------------------------------------- 1 | (ns examples.verify.core 2 | (:require [om.core :as om :include-macros true] 3 | [om.dom :as dom :include-macros true])) 4 | 5 | (enable-console-print!) 6 | 7 | (defn mincase [data owner] 8 | (reify 9 | om/IWillUpdate 10 | (will-update [this next-props next-state] 11 | (.log js/console "om/IWillUpdate invoked")) 12 | om/IRender 13 | (render [_] 14 | (dom/div #js {:className "mincase"} 15 | (when (:click-to-fail data) (dom/span nil "Clicked!")) 16 | (dom/a 17 | #js {:onClick #(om/update! data :click-to-fail :done)} 18 | "Click me to trigger failure"))))) 19 | 20 | (om/root mincase 21 | (atom {}) 22 | {:target (.getElementById js/document "app")}) 23 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 2 | <modelVersion>4.0.0</modelVersion> 3 | <groupId>org.omcljs</groupId> 4 | <artifactId>om</artifactId> 5 | <packaging>jar</packaging> 6 | <version>dev</version> 7 | <name>om</name> 8 | <description>A functional UI framework</description> 9 | <url>http://github.com/omcljs/om</url> 10 | <licenses> 11 | <license> 12 | <name>The Apache Software License, Version 2.0</name> 13 | <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url> 14 | <distribution>repo</distribution> 15 | </license> 16 | </licenses> 17 | <scm> 18 | <connection>scm:git:git://github.com/omcljs/om.git</connection> 19 | <developerConnection>scm:git:ssh://git@github.com/omcljs/om.git</developerConnection> 20 | <url>https://github.com/omcljs/om</url> 21 | </scm> 22 | <build> 23 | <sourceDirectory>src/main</sourceDirectory> 24 | <resources> 25 | <resource> 26 | <directory>resources</directory> 27 | </resource> 28 | <resource> 29 | <directory>src/devcards</directory> 30 | </resource> 31 | </resources> 32 | <testSourceDirectory>src/test</testSourceDirectory> 33 | <!-- 34 | <directory>target</directory> 35 | <outputDirectory>target/classes</outputDirectory> 36 | --> 37 | <plugins> 38 | </plugins> 39 | </build> 40 | <repositories> 41 | <repository> 42 | <id>central</id> 43 | <url>http://repo1.maven.org/maven2/</url> 44 | <snapshots> 45 | <enabled>false</enabled> 46 | </snapshots> 47 | <releases> 48 | <enabled>true</enabled> 49 | </releases> 50 | </repository> 51 | <repository> 52 | <id>clojars</id> 53 | <url>https://clojars.org/repo/</url> 54 | <snapshots> 55 | <enabled>true</enabled> 56 | </snapshots> 57 | <releases> 58 | <enabled>true</enabled> 59 | </releases> 60 | </repository> 61 | </repositories> 62 | <dependencies> 63 | <dependency> 64 | <groupId>org.clojure</groupId> 65 | <artifactId>clojure</artifactId> 66 | <version>1.9.0</version> 67 | <scope>provided</scope> 68 | </dependency> 69 | <dependency> 70 | <groupId>org.clojure</groupId> 71 | <artifactId>clojurescript</artifactId> 72 | <version>1.9.908</version> 73 | <classifier>aot</classifier> 74 | <scope>provided</scope> 75 | <exclusions> 76 | <exclusion> 77 | <groupId>org.clojure</groupId> 78 | <artifactId>data.json</artifactId> 79 | </exclusion> 80 | </exclusions> 81 | </dependency> 82 | <dependency> 83 | <groupId>org.clojure</groupId> 84 | <artifactId>data.json</artifactId> 85 | <version>0.2.6</version> 86 | <classifier>aot</classifier> 87 | <scope>provided</scope> 88 | </dependency> 89 | <dependency> 90 | <groupId>org.clojure</groupId> 91 | <artifactId>core.async</artifactId> 92 | <version>0.3.443</version> 93 | <scope>test</scope> 94 | <exclusions> 95 | <exclusion> 96 | <groupId>org.clojure</groupId> 97 | <artifactId>tools.reader</artifactId> 98 | </exclusion> 99 | </exclusions> 100 | </dependency> 101 | <dependency> 102 | <groupId>com.cognitect</groupId> 103 | <artifactId>transit-clj</artifactId> 104 | <version>0.8.300</version> 105 | </dependency> 106 | <dependency> 107 | <groupId>com.cognitect</groupId> 108 | <artifactId>transit-cljs</artifactId> 109 | <version>0.8.239</version> 110 | </dependency> 111 | <dependency> 112 | <groupId>cljsjs</groupId> 113 | <artifactId>react</artifactId> 114 | <version>16.0.0-0</version> 115 | </dependency> 116 | <dependency> 117 | <groupId>cljsjs</groupId> 118 | <artifactId>react-dom</artifactId> 119 | <version>16.0.0-0</version> 120 | </dependency> 121 | <dependency> 122 | <groupId>org.clojure</groupId> 123 | <artifactId>tools.nrepl</artifactId> 124 | <version>0.2.13</version> 125 | <scope>test</scope> 126 | </dependency> 127 | <dependency> 128 | <groupId>figwheel-sidecar</groupId> 129 | <artifactId>figwheel-sidecar</artifactId> 130 | <version>0.5.10</version> 131 | <scope>test</scope> 132 | <exclusions> 133 | <exclusion> 134 | <groupId>org.clojure</groupId> 135 | <artifactId>clojurescript</artifactId> 136 | </exclusion> 137 | <exclusion> 138 | <groupId>org.clojure</groupId> 139 | <artifactId>tools.reader</artifactId> 140 | </exclusion> 141 | </exclusions> 142 | </dependency> 143 | <dependency> 144 | <groupId>devcards</groupId> 145 | <artifactId>devcards</artifactId> 146 | <version>0.2.4</version> 147 | <scope>test</scope> 148 | <exclusions> 149 | <exclusion> 150 | <groupId>org.clojure</groupId> 151 | <artifactId>clojurescript</artifactId> 152 | </exclusion> 153 | </exclusions> 154 | </dependency> 155 | </dependencies> 156 | <developers> 157 | <developer> 158 | <name>David Nolen</name> 159 | <email>david.nolen@gmail.com</email> 160 | </developer> 161 | </developers> 162 | </project> 163 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject org.omcljs/om "1.0.0-beta4" 2 | :description "ClojureScript interface to Facebook's React" 3 | :url "http://github.com/swannodette/om" 4 | :license {:name "Eclipse" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | 7 | :repositories [["clojars" {:sign-releases false}]] 8 | 9 | :jvm-opts ^:replace ["-Xms512m" "-Xmx512m" "-server"] 10 | 11 | :source-paths ["src/main" "src/devcards" "src/test"] 12 | 13 | :dependencies [[org.clojure/clojure "1.9.0-alpha19" :scope "provided"] 14 | [org.clojure/clojurescript "1.9.908" :scope "provided" :classifier "aot" 15 | :exclusions [org.clojure/clojure 16 | org.clojure/data.json]] 17 | [org.clojure/data.json "0.2.6" :scope "provided" :classifier "aot"] 18 | [cljsjs/react "16.0.0-0"] 19 | [cljsjs/react-dom "16.0.0-0"] 20 | [com.cognitect/transit-clj "0.8.300"] 21 | [com.cognitect/transit-cljs "0.8.239"] 22 | 23 | [org.clojure/core.async "0.2.385" :scope "test" 24 | :exclusions [org.clojure/tools.reader]] 25 | [figwheel-sidecar "0.5.10" :scope "test" 26 | :exclusions [org.clojure/clojurescript 27 | org.clojure/tools.reader]] 28 | [devcards "0.2.4" :scope "test" 29 | :exclusions [org.clojure/clojurescript]]] 30 | 31 | :plugins [[lein-cljsbuild "1.1.6"]] 32 | 33 | :jar-exclusions [#".DS_Store" #"dev" #"devcards" #"test" #"index.html" 34 | #"main" #"public"] 35 | 36 | :clean-targets ^{:protect false} ["resources/out"] 37 | 38 | :cljsbuild { 39 | :builds [{:id "dev" 40 | :source-paths ["src/main" "src/dev"] 41 | :compiler {:main om.dev 42 | :asset-path "out" 43 | :output-to "resources/out/app.js" 44 | :output-dir "resources/out" 45 | :optimizations :none}} 46 | {:id "test" 47 | :source-paths ["src" "test"] 48 | :compiler { 49 | :output-to "script/tests.simple.js" 50 | :output-dir "script/out" 51 | :source-map "script/tests.simple.js.map" 52 | :output-wrapper false 53 | :optimizations :simple}} 54 | ;; examples 55 | {:id "hello" 56 | :source-paths ["src" "examples/hello/src"] 57 | :compiler { 58 | :output-to "examples/hello/main.js" 59 | :output-dir "examples/hello/out" 60 | :source-map true 61 | :optimizations :none}} 62 | {:id "state-bug" 63 | :source-paths ["src" "examples/state_bug/src"] 64 | :compiler { 65 | :main examples.state-bug.core 66 | :asset-path "out" 67 | :output-to "examples/state_bug/main.js" 68 | :output-dir "examples/state_bug/out" 69 | :source-map true 70 | :optimizations :none}} 71 | {:id "verify" 72 | :source-paths ["src" "examples/verify/src"] 73 | :compiler { 74 | :output-to "examples/verify/main.js" 75 | :output-dir "examples/verify/out" 76 | :source-map true 77 | :optimizations :none}} 78 | {:id "input" 79 | :source-paths ["src" "examples/input/src"] 80 | :compiler { 81 | :output-to "examples/input/main.js" 82 | :output-dir "examples/input/out" 83 | :source-map true 84 | :optimizations :none}} 85 | {:id "multi" 86 | :source-paths ["src" "examples/multi/src"] 87 | :compiler { 88 | :output-to "examples/multi/main.js" 89 | :output-dir "examples/multi/out" 90 | :source-map true 91 | :optimizations :none}} 92 | {:id "cursor-as-key" 93 | :source-paths ["src" "examples/cursor_as_key/src"] 94 | :compiler { 95 | :output-to "examples/cursor_as_key/main.js" 96 | :output-dir "examples/cursor_as_key/out" 97 | :source-map true 98 | :optimizations :none}} 99 | {:id "unmount" 100 | :source-paths ["src" "examples/unmount/src"] 101 | :compiler { 102 | :output-to "examples/unmount/main.js" 103 | :output-dir "examples/unmount/out" 104 | :source-map true 105 | :optimizations :none}} 106 | {:id "mouse" 107 | :source-paths ["src" "examples/mouse/src"] 108 | :compiler { 109 | :output-to "examples/mouse/main.js" 110 | :output-dir "examples/mouse/out" 111 | :source-map true 112 | :optimizations :none}} 113 | {:id "multiroot" 114 | :source-paths ["src" "examples/multiroot/src"] 115 | :compiler { 116 | :output-to "examples/multiroot/main.js" 117 | :output-dir "examples/multiroot/out" 118 | :source-map true 119 | :optimizations :none}} 120 | {:id "counters" 121 | :source-paths ["src" "examples/counters/src"] 122 | :compiler { 123 | :output-to "examples/counters/main.js" 124 | :output-dir "examples/counters/out" 125 | :source-map true 126 | :optimizations :none}} 127 | {:id "animation" 128 | :source-paths ["src" "examples/animation/src"] 129 | :compiler { 130 | :output-to "examples/animation/main.js" 131 | :output-dir "examples/animation/out" 132 | :source-map true 133 | :optimizations :none}} 134 | {:id "shared" 135 | :source-paths ["src" "examples/shared/src"] 136 | :compiler { 137 | :output-to "examples/shared/main.js" 138 | :output-dir "examples/shared/out" 139 | :source-map true 140 | :optimizations :none}} 141 | {:id "typeahead" 142 | :source-paths ["src" "examples/typeahead/src"] 143 | :compiler { 144 | :output-to "examples/typeahead/main.js" 145 | :output-dir "examples/typeahead/out" 146 | :source-map true 147 | :optimizations :none}} 148 | {:id "sortable" 149 | :source-paths ["src" "examples/sortable/src"] 150 | :compiler { 151 | :output-to "examples/sortable/main.js" 152 | :output-dir "examples/sortable/out" 153 | :source-map true 154 | :optimizations :none}} 155 | {:id "instrument" 156 | :source-paths ["src" "examples/instrument/src"] 157 | :compiler { 158 | :output-to "examples/instrument/main.js" 159 | :output-dir "examples/instrument/out" 160 | :source-map true 161 | :optimizations :none}} 162 | {:id "stateful" 163 | :source-paths ["src" "examples/stateful/src"] 164 | :compiler { 165 | :output-to "examples/stateful/main.js" 166 | :output-dir "examples/stateful/out" 167 | :source-map true 168 | :optimizations :none}} 169 | {:id "harmful" 170 | :source-paths ["src" "examples/harmful/src"] 171 | :compiler { 172 | :output-to "examples/harmful/main.js" 173 | :output-dir "examples/harmful/out" 174 | :source-map true 175 | :optimizations :none}} 176 | {:id "mixins" 177 | :source-paths ["src" "examples/mixins/src"] 178 | :compiler { 179 | :output-to "examples/mixins/main.js" 180 | :output-dir "examples/mixins/out" 181 | :source-map true 182 | :optimizations :none}} 183 | {:id "two-lists" 184 | :source-paths ["src" "examples/two_lists/src"] 185 | :compiler { 186 | :output-to "examples/two_lists/main.js" 187 | :output-dir "examples/two_lists/out" 188 | :source-map true 189 | :optimizations :none}} 190 | {:id "update-props" 191 | :source-paths ["src" "examples/update_props/src"] 192 | :compiler { 193 | :output-to "examples/update_props/main.js" 194 | :output-dir "examples/update_props/out" 195 | :source-map true 196 | :optimizations :none}} 197 | {:id "refs" 198 | :source-paths ["src" "examples/refs/src"] 199 | :compiler { 200 | :output-to "examples/refs/main.js" 201 | :output-dir "examples/refs/out" 202 | :source-map true 203 | :optimizations :none}} 204 | {:id "tests" 205 | :source-paths ["src" "examples/tests/src"] 206 | :compiler { 207 | :output-to "examples/tests/main.js" 208 | :output-dir "examples/tests/out" 209 | :source-map true 210 | :optimizations :none}}]}) 211 | -------------------------------------------------------------------------------- /resources/index.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html> 3 | <head lang="en"> 4 | <meta charset="UTF-8"> 5 | <title></title> 6 | </head> 7 | <body> 8 | <div id="app"></div> 9 | <script src="out/app.js"></script> 10 | </body> 11 | </html> -------------------------------------------------------------------------------- /resources/public/devcards/index.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html> 3 | <head lang="en"> 4 | <meta charset="UTF-8"> 5 | <title></title> 6 | </head> 7 | <body> 8 | <div id="app"></div> 9 | <script src="main.js"></script> 10 | </body> 11 | </html> -------------------------------------------------------------------------------- /resources/public/devcards/main.cljs.edn: -------------------------------------------------------------------------------- 1 | {:require [om.devcards.core] 2 | :compiler-options {:asset-path "main.out"}} 3 | -------------------------------------------------------------------------------- /script/build.clj: -------------------------------------------------------------------------------- 1 | (require '[cljs.build.api :as b]) 2 | 3 | (b/build (b/inputs "src/main" "src/devcards") 4 | {:main 'om.devcards.core 5 | :asset-path "/devcards/out" 6 | :output-to "resources/public/devcards/main.js" 7 | :output-dir "resources/public/devcards/out" 8 | :parallel-build true 9 | :compiler-stats true 10 | :verbose true}) 11 | 12 | (System/exit 0) 13 | -------------------------------------------------------------------------------- /script/figwheel.clj: -------------------------------------------------------------------------------- 1 | (require '[figwheel-sidecar.repl :as r] 2 | '[figwheel-sidecar.repl-api :as ra]) 3 | 4 | (ra/start-figwheel! 5 | {:figwheel-options {} 6 | :build-ids ["devcards"] 7 | :all-builds 8 | [{:id "devcards" 9 | :figwheel {:devcards true} 10 | :source-paths ["src/main" "src/devcards"] 11 | :compiler {:main 'om.devcards.core 12 | :asset-path "/devcards/out" 13 | :output-to "resources/public/devcards/main.js" 14 | :output-dir "resources/public/devcards/out" 15 | :parallel-build true 16 | :compiler-stats true 17 | :verbose true}}]}) 18 | 19 | (ra/cljs-repl) 20 | -------------------------------------------------------------------------------- /script/index.html: -------------------------------------------------------------------------------- 1 | <html> 2 | <body> 3 | <script src="tests.simple.js"></script> 4 | </body> 5 | </html> 6 | -------------------------------------------------------------------------------- /script/repl.clj: -------------------------------------------------------------------------------- 1 | (require '[cljs.repl :as repl]) 2 | (require '[cljs.repl.node :as node]) 3 | 4 | (cljs.repl/repl* (node/repl-env) 5 | {:verbose true 6 | :parallel-build true 7 | :compiler-stats true}) 8 | -------------------------------------------------------------------------------- /script/test.clj: -------------------------------------------------------------------------------- 1 | (require '[cljs.build.api :as b]) 2 | 3 | (b/build (b/inputs "src/main" "src/test") 4 | {:target :nodejs 5 | :main 'om.next.run-tests 6 | :output-to "target/test/test.js" 7 | :output-dir "target/test/out" 8 | :parallel-build true 9 | :compiler-stats true 10 | :static-fns true 11 | :verbose true}) 12 | 13 | (System/exit 0) 14 | -------------------------------------------------------------------------------- /script/watch.clj: -------------------------------------------------------------------------------- 1 | (require '[cljs.build.api :as b]) 2 | 3 | (b/watch (b/inputs "src/main" "src/test/om/next") 4 | {:main 'om.next.tests 5 | :output-to "target/test/main.js" 6 | :output-dir "target/test/out" 7 | :parallel-build true 8 | :compiler-stats true 9 | :verbose true}) 10 | 11 | (System/exit 0) -------------------------------------------------------------------------------- /src/devcards/om/devcards/autocomplete.cljs: -------------------------------------------------------------------------------- 1 | (ns om.devcards.autocomplete 2 | (:require-macros [cljs.core.async.macros :refer [go]]) 3 | (:require [cljs.core.async :as async :refer [<! >! put! chan]] 4 | [devcards.core :refer-macros [defcard deftest dom-node]] 5 | [clojure.string :as string] 6 | [om.next :as om :refer-macros [defui]] 7 | [om.dom :as dom]) 8 | (:import [goog Uri] 9 | [goog.net Jsonp])) 10 | 11 | (enable-console-print!) 12 | 13 | (def base-url 14 | "http://en.wikipedia.org/w/api.php?action=opensearch&format=json&search=") 15 | 16 | (defn jsonp 17 | ([uri] (jsonp (chan) uri)) 18 | ([c uri] 19 | (let [gjsonp (Jsonp. (Uri. uri))] 20 | (.send gjsonp nil #(put! c %)) 21 | c))) 22 | 23 | ;; ----------------------------------------------------------------------------- 24 | ;; Parsing 25 | 26 | (defmulti read om/dispatch) 27 | 28 | (defmethod read :search/results 29 | [{:keys [state ast] :as env} k {:keys [query]}] 30 | (merge 31 | {:value (get @state k [])} 32 | (when-not (and (string/blank? query) 33 | (<= 2 (count query))) 34 | {:search ast}))) 35 | 36 | ;; ----------------------------------------------------------------------------- 37 | ;; App 38 | 39 | (defn result-list [results] 40 | (dom/ul #js {:key "result-list"} 41 | (map #(dom/li nil %) results))) 42 | 43 | (defn search-field [ac query] 44 | (dom/input 45 | #js {:key "search-field" 46 | :value query 47 | :onChange 48 | (fn [e] 49 | (om/set-query! ac 50 | {:params {:query (.. e -target -value)}}))})) 51 | 52 | (defui AutoCompleter 53 | static om/IQueryParams 54 | (params [_] 55 | {:query ""}) 56 | static om/IQuery 57 | (query [_] 58 | '[(:search/results {:query ?query})]) 59 | Object 60 | (render [this] 61 | (let [{:keys [search/results]} (om/props this)] 62 | (dom/div nil 63 | (dom/h2 nil "Autocompleter") 64 | (cond-> 65 | [(search-field this (:query (om/get-params this)))] 66 | (not (empty? results)) (conj (result-list results))))))) 67 | 68 | (defn search-loop [c] 69 | (go 70 | (loop [[query cb remote] (<! c)] 71 | (if-not (empty? query) 72 | (let [[_ results] (<! (jsonp (str base-url query)))] 73 | (cb {:search/results results} query remote)) 74 | (cb {:search/results []} query remote)) 75 | (recur (<! c))))) 76 | 77 | (defn send-to-chan [c] 78 | (fn [{:keys [search]} cb] 79 | (when search 80 | (let [{[search] :children} (om/query->ast search) 81 | query (get-in search [:params :query])] 82 | (put! c [query cb :search]))))) 83 | 84 | (def send-chan (chan)) 85 | 86 | (def reconciler 87 | (om/reconciler 88 | {:state {:search/results []} 89 | :parser (om/parser {:read read}) 90 | :send (send-to-chan send-chan) 91 | :remotes [:remote :search]})) 92 | 93 | (search-loop send-chan) 94 | 95 | (defcard test-autocomplete 96 | "Demonstrate simple autocompleter" 97 | (dom-node 98 | (fn [_ node] 99 | (om/add-root! reconciler AutoCompleter node)))) 100 | -------------------------------------------------------------------------------- /src/devcards/om/devcards/core.cljs: -------------------------------------------------------------------------------- 1 | (ns om.devcards.core 2 | (:require [cljs.test :refer-macros [is async]] 3 | [devcards.core :refer-macros [defcard deftest dom-node start-devcard-ui!]] 4 | [cljs.pprint :as pprint] 5 | [om.devcards.utils :as utils] 6 | [om.devcards.tutorials] 7 | [om.devcards.bugs] 8 | [om.devcards.autocomplete] 9 | [om.devcards.shared-fn-test] 10 | [om.next :as om :refer-macros [defui]] 11 | [om.dom :as dom])) 12 | 13 | (enable-console-print!) 14 | (start-devcard-ui!) 15 | 16 | (defui Hello 17 | Object 18 | (render [this] 19 | (dom/p nil (-> this om/props :text)))) 20 | 21 | (def hello (om/factory Hello)) 22 | 23 | (defcard simple-component 24 | "Test that Om Next component work as regular React components." 25 | (hello {:text "Hello, world!"})) 26 | 27 | (def p 28 | (om/parser 29 | {:read (fn [_ _ _] {:remote true}) 30 | :mutate (fn [_ _ _] {:remote true})})) 31 | 32 | (def r 33 | (om/reconciler 34 | {:parser p 35 | :ui->ref (fn [c] (-> c om/props :id))})) 36 | 37 | (defui Binder 38 | Object 39 | (componentDidMount [this] 40 | (let [indexes @(get-in (-> this om/props :reconciler) [:config :indexer])] 41 | (om/update-state! this assoc :indexes indexes))) 42 | (render [this] 43 | (binding [om/*reconciler* (-> this om/props :reconciler)] 44 | (apply dom/div nil 45 | (hello {:id 0 :text "Goodbye, world!"}) 46 | (when-let [indexes (get-in (om/get-state this) 47 | [:indexes :ref->components])] 48 | [(dom/p nil (pr-str indexes))]))))) 49 | 50 | (def binder (om/factory Binder)) 51 | 52 | (defcard basic-nested-component 53 | "Test that component nesting works" 54 | (binder {:reconciler r})) 55 | 56 | (deftest test-indexer 57 | "Test indexer" 58 | (let [idxr (get-in r [:config :indexer])] 59 | (is (not (nil? idxr)) "Indexer is not nil in the reconciler") 60 | (is (not (nil? @idxr)) "Indexer is IDeref"))) 61 | 62 | ;; ----------------------------------------------------------------------------- 63 | ;; Counters 64 | 65 | (defmulti counters-read (fn [_ k _] k)) 66 | 67 | (defmulti counters-mutate (fn [_ k _] k)) 68 | 69 | (defmethod counters-read :default 70 | [{:keys [state]} k params] 71 | (let [st @state] 72 | (if (contains? st k) 73 | {:value (get st k)} 74 | {:remote true}))) 75 | 76 | (defmethod counters-read :counters/list 77 | [{:keys [state query]} _ _] 78 | (let [st @state 79 | xf (map #(select-keys (get-in st %) query))] 80 | {:value (into [] xf (:counters/list st))})) 81 | 82 | (defmethod counters-mutate 'counter/increment 83 | [{:keys [state ref]} _ _] 84 | {:value {:keys []} 85 | :action 86 | (fn [] 87 | (swap! state update-in (conj ref :counter/count) inc))}) 88 | 89 | (defmethod counters-mutate 'counters/delete 90 | [{:keys [state ref]} _ _] 91 | {:value {:keys [:counters/list]} 92 | :action 93 | (fn [] 94 | (swap! state 95 | (fn [state] 96 | (-> state 97 | (update-in (pop ref) dissoc (peek ref)) 98 | (update-in [:counters/list] #(vec (remove #{ref} %)))))))}) 99 | 100 | (defmethod counters-mutate 'counters/create 101 | [{:keys [state]} _ _] 102 | {:value {:keys [:counters/list]} 103 | :action 104 | (fn [] 105 | (swap! state 106 | (fn [state] 107 | (let [id (:app/current-id state) 108 | counter {:id id :counter/count 0}] 109 | (-> state 110 | (assoc-in [:app/counters id] counter) 111 | (update-in [:counters/list] conj [:app/counters id]) 112 | (update-in [:app/current-id] inc))))))}) 113 | 114 | ;; ----------------------------------------------------------------------------- 115 | ;; Counter 116 | 117 | (defui Counter 118 | static om/IQuery 119 | (query [this] 120 | '[:id :counter/count]) 121 | static om/Ident 122 | (ident [this {:keys [id]}] 123 | [:app/counters id]) 124 | Object 125 | (initLocalState [this] 126 | {:state-count 0}) 127 | (componentWillUnmount [this] 128 | (println "Buh-bye!")) 129 | (componentWillUpdate [this next-props next-state] 130 | (println "component will update" (om/props this) next-props)) 131 | (componentDidUpdate [this prev-props prev-state] 132 | #_(println "component did update" (om/props this) prev-props)) 133 | (render [this] 134 | (println "Shared Counter" (om/shared this)) 135 | (let [{:keys [:counter/count] :as props} (om/props this)] 136 | (dom/div nil 137 | (dom/p nil 138 | (str "Count: " count 139 | " State Count: " (om/get-state this :state-count))) 140 | (dom/button 141 | #js {:onClick 142 | (fn [_] (om/transact! this '[(counter/increment)]))} 143 | "Update Props, Click Me!") 144 | (dom/button 145 | #js {:style #js {:marginLeft "10px"} 146 | :onClick 147 | (fn [_] (om/update-state! this update-in [:state-count] inc))} 148 | "Update State, Click Me!") 149 | (dom/button 150 | #js {:style #js {:marginLeft "10px"} 151 | :onClick 152 | (fn [_] (om/transact! this '[(counters/delete) :counters/list]))} 153 | "Delete"))))) 154 | 155 | (def counter (om/factory Counter {:keyfn :id})) 156 | 157 | ;; ----------------------------------------------------------------------------- 158 | ;; CountersAppTitle 159 | 160 | (defui CountersAppTitle 161 | Object 162 | (render [this] 163 | (apply dom/div nil 164 | (om/children this)))) 165 | 166 | (def counters-app-title (om/factory CountersAppTitle)) 167 | 168 | ;; ----------------------------------------------------------------------------- 169 | ;; CountersApp 170 | 171 | (defui CountersApp 172 | static om/IQueryParams 173 | (params [this] 174 | {:counter (om/get-query Counter)}) 175 | static om/IQuery 176 | (query [this] 177 | '[:app/title {:counters/list ?counter}]) 178 | Object 179 | (render [this] 180 | (println "Shared CountersApp" (om/shared this)) 181 | (let [{:keys [:app/title :counters/list] :as props} 182 | (om/props this)] 183 | (apply dom/div nil 184 | (counters-app-title nil 185 | (dom/h2 #js {:key "a"} "Hello World!") 186 | (dom/h3 #js {:key "b"} "cool stuff")) 187 | (dom/div nil 188 | (dom/button 189 | #js {:onClick (fn [e] (om/transact! this '[(counters/create)]))} 190 | "Add Counter!")) 191 | (map counter list))))) 192 | 193 | ;; ----------------------------------------------------------------------------- 194 | ;; Reconciler setup 195 | 196 | (def counters-app-state 197 | (atom {:app/title "Hello World!" 198 | :app/current-id 3 199 | :app/counters 200 | {0 {:id 0 :counter/count 0} 201 | 1 {:id 1 :counter/count 0} 202 | 2 {:id 2 :counter/count 0}} 203 | :counters/list [[:app/counters 0] 204 | [:app/counters 1] 205 | [:app/counters 2]]})) 206 | 207 | (def counters-reconciler 208 | (om/reconciler 209 | {:state counters-app-state 210 | :parser (om/parser {:read counters-read :mutate counters-mutate}) 211 | :shared-fn (fn [_] {:foo :bar})})) 212 | 213 | (defcard test-counters 214 | "Test that we can mock a reconciler backed Om Next component into devcards" 215 | (dom-node 216 | (fn [_ node] 217 | (om/add-root! counters-reconciler CountersApp node)))) 218 | 219 | (defcard test-counters-atom 220 | (om/app-state counters-reconciler)) 221 | 222 | ;; ----------------------------------------------------------------------------- 223 | ;; Children 224 | 225 | (defui Children 226 | Object 227 | (render [this] 228 | (dom/div nil 229 | (map identity 230 | #js [(dom/div nil "Foo") 231 | (dom/div nil "Bar") 232 | (map identity 233 | #js [(dom/div nil "Bar") 234 | (dom/div nil "Woz")])])))) 235 | 236 | (def children (om/factory Children)) 237 | 238 | (defcard test-lazy-children 239 | "Test that lazy sequences as elements works. This permits React.js style 240 | splicing." 241 | (children)) 242 | 243 | ;; ----------------------------------------------------------------------------- 244 | ;; Simple Recursive Query Syntax 245 | 246 | (def simple-tree-data 247 | {:tree {:node-value 1 248 | :children [{:node-value 2 249 | :children [{:node-value 3 250 | :children []}]} 251 | {:node-value 4 252 | :children []}]}}) 253 | 254 | (declare simple-node) 255 | 256 | (defui SimpleNode 257 | static om/IQuery 258 | (query [this] 259 | '[:node-value {:children ...}]) 260 | Object 261 | (render [this] 262 | (let [{:keys [node-value children]} (om/props this)] 263 | (dom/li nil 264 | (dom/div nil (str "Node value:" node-value)) 265 | (dom/ul nil 266 | (map simple-node children)))))) 267 | 268 | (def simple-node (om/factory SimpleNode)) 269 | 270 | (defui SimpleTree 271 | static om/IQuery 272 | (query [this] 273 | [{:tree (om/get-query SimpleNode)}]) 274 | Object 275 | (render [this] 276 | (let [{:keys [tree]} (om/props this)] 277 | (dom/ul nil 278 | (simple-node tree))))) 279 | 280 | (defmulti simple-tree-read om/dispatch) 281 | 282 | (defmethod simple-tree-read :node-value 283 | [{:keys [data] :as env} _ _] 284 | {:value (:node-value data)}) 285 | 286 | (defmethod simple-tree-read :children 287 | [{:keys [data parser query] :as env} _ _] 288 | {:value (let [f #(parser (assoc env :data %) query)] 289 | (into [] (map f (:children data))))}) 290 | 291 | (defmethod simple-tree-read :tree 292 | [{:keys [state parser query] :as env} k _] 293 | (let [st @state] 294 | {:value (parser (assoc env :data (:tree st)) query)})) 295 | 296 | (def simple-tree-reconciler 297 | (om/reconciler 298 | {:state (atom simple-tree-data) 299 | :normalize false 300 | :parser (om/parser {:read simple-tree-read})})) 301 | 302 | (defcard test-simple-recursive-syntax 303 | "Test that `'[:node-value {:children ...}]` syntax works." 304 | (dom-node 305 | (fn [_ node] 306 | (om/add-root! simple-tree-reconciler SimpleTree node)))) 307 | 308 | ;; ----------------------------------------------------------------------------- 309 | ;; Recursive Query Syntax with Mutations 310 | 311 | (def norm-tree-data 312 | {:tree {:id 0 313 | :node-value 1 314 | :children [{:id 1 315 | :node-value 2 316 | :children [{:id 2 317 | :node-value 3 318 | :children []}]} 319 | {:id 3 320 | :node-value 4 321 | :children []}]}}) 322 | 323 | (declare norm-node) 324 | 325 | (defmulti norm-tree-read om/dispatch) 326 | 327 | (defmethod norm-tree-read :tree 328 | [{:keys [state query] :as env} _ _] 329 | (let [st @state] 330 | {:value (om/db->tree query (:tree st) st)})) 331 | 332 | (defmethod norm-tree-read :node/by-id 333 | [{:keys [state query query-root]} _ _] 334 | {:value (om/db->tree query query-root @state)}) 335 | 336 | (defmulti norm-tree-mutate om/dispatch) 337 | 338 | (defmethod norm-tree-mutate 'tree/increment 339 | [{:keys [state]} _ {:keys [id]}] 340 | {:action 341 | (fn [] 342 | (swap! state update-in [:node/by-id id :node-value] inc))}) 343 | 344 | (defmethod norm-tree-mutate 'tree/decrement 345 | [{:keys [state]} _ {:keys [id]}] 346 | {:action 347 | (fn [] 348 | (swap! state update-in [:node/by-id id :node-value] 349 | (fn [n] (max 0 (dec n)))))}) 350 | 351 | (defn increment! [c id] 352 | (fn [e] 353 | (om/transact! c `[(tree/increment {:id ~id})]))) 354 | 355 | (defn decrement! [c id] 356 | (fn [e] 357 | (om/transact! c `[(tree/decrement {:id ~id})]))) 358 | 359 | (defui NormNode 360 | static om/Ident 361 | (ident [this {:keys [id]}] 362 | [:node/by-id id]) 363 | static om/IQuery 364 | (query [this] 365 | '[:id :node-value {:children ...}]) 366 | Object 367 | (render [this] 368 | (let [{:keys [id node-value children]} (om/props this)] 369 | (dom/li nil 370 | (dom/div nil 371 | (dom/label nil (str "Node value:" node-value)) 372 | (dom/button #js {:onClick (increment! this id)} "+") 373 | (dom/button #js {:onClick (decrement! this id)} "-")) 374 | (dom/ul nil 375 | (map norm-node children)))))) 376 | 377 | (def norm-node (om/factory NormNode)) 378 | 379 | (defui NormTree 380 | static om/IQuery 381 | (query [this] 382 | [{:tree (om/get-query NormNode)}]) 383 | Object 384 | (render [this] 385 | (let [{:keys [tree]} (om/props this)] 386 | (dom/ul nil 387 | (norm-node tree))))) 388 | 389 | (def norm-tree-parser 390 | (om/parser {:read norm-tree-read 391 | :mutate norm-tree-mutate})) 392 | 393 | (def norm-tree-reconciler 394 | (om/reconciler 395 | {:state norm-tree-data 396 | :parser norm-tree-parser 397 | :pathopt true})) 398 | 399 | (defcard test-simple-recursive-syntax-with-mutation 400 | "Test that simple recursive syntax works with mutations and component 401 | local state. Cool" 402 | (dom-node 403 | (fn [_ node] 404 | (om/add-root! norm-tree-reconciler NormTree node)))) 405 | 406 | (comment 407 | (pprint/pprint @(-> norm-tree-reconciler :config :indexer)) 408 | ) 409 | 410 | ;; ----------------------------------------------------------------------------- 411 | ;; Instrumenting 412 | 413 | (defui Wrapper 414 | Object 415 | (render [this] 416 | (let [{:keys [factory props]} (om/props this)] 417 | (dom/div nil 418 | (dom/h2 nil "Wrapped") 419 | (factory props))))) 420 | 421 | (def wrapper (om/factory Wrapper)) 422 | 423 | (defui AView 424 | Object 425 | (render [this] 426 | (dom/div nil "I'm a view!"))) 427 | 428 | (def aview (om/factory AView)) 429 | 430 | (defui RootView 431 | Object 432 | (render [this] 433 | (aview {}))) 434 | 435 | (def instrument-reconciler 436 | (om/reconciler 437 | {:state {} 438 | :parser (fn [_ _ _] {:value :not-found}) 439 | :instrument (fn [{:keys [props _ class factory]}] 440 | (println "instrument!" (= AView class) 441 | AView class) 442 | (if (= AView class) 443 | (wrapper {:factory (om/factory AView {:instrument? false}) :props props}) 444 | (factory props)))})) 445 | 446 | (defcard test-instrument 447 | "Test that instrument interception works" 448 | (dom-node 449 | (fn [_ node] 450 | (om/add-root! instrument-reconciler RootView node)))) 451 | -------------------------------------------------------------------------------- /src/devcards/om/devcards/shared_fn_test.cljs: -------------------------------------------------------------------------------- 1 | (ns om.devcards.shared-fn-test 2 | (:require [devcards.core :refer-macros [defcard deftest dom-node]] 3 | [om.next :as om :refer-macros [defui]] 4 | [om.dom :as dom])) 5 | 6 | (defui Home 7 | static om/IQuery 8 | (query [this] [:counter]) 9 | 10 | Object 11 | (render [this] 12 | (let [shared (om/shared this) 13 | props (om/props this)] 14 | (println "Render Root!") 15 | (dom/div nil 16 | (dom/h3 nil (str "Props: " props)) 17 | (dom/h3 nil (str "Shared: " shared)) 18 | (dom/button 19 | #js {:onClick #(om/transact! this '[(my/test) :counter])} 20 | "Increment!"))))) 21 | 22 | (def app-state (atom {:counter 0})) 23 | 24 | (defn read 25 | [env key params] 26 | (let [{:keys [state]} env] 27 | {:value (get @state key)})) 28 | 29 | (defn mutate 30 | [env key params] 31 | (let [{:keys [state]} env] 32 | {:value {:keys [:counter]} 33 | :action #(swap! state update-in [:counter] inc)})) 34 | 35 | (def reconciler 36 | (om/reconciler 37 | {:state app-state 38 | :parser (om/parser {:read read :mutate mutate}) 39 | :shared {} 40 | :shared-fn (fn [root-props] 41 | root-props)})) 42 | 43 | (defcard test-om-478 44 | "Test that re-running shared-fn works" 45 | (dom-node 46 | (fn [_ node] 47 | (om/add-root! reconciler Home node)))) 48 | -------------------------------------------------------------------------------- /src/devcards/om/devcards/tutorials.cljs: -------------------------------------------------------------------------------- 1 | (ns om.devcards.tutorials 2 | (:require [cljs.test :refer-macros [is async]] 3 | [devcards.core :refer-macros [defcard deftest dom-node]] 4 | [om.next :as om :refer-macros [defui]] 5 | [om.dom :as dom])) 6 | 7 | ;; ============================================================================= 8 | ;; Quick Start 9 | 10 | (def animals-app-state 11 | (atom 12 | {:app/title "Animals" 13 | :animals/list 14 | [[1 "Ant"] [2 "Antelope"] [3 "Bird"] [4 "Cat"] [5 "Dog"] 15 | [6 "Lion"] [7 "Mouse"] [8 "Monkey"] [9 "Snake"] [10 "Zebra"]]})) 16 | 17 | (defmulti animals-read (fn [env key params] key)) 18 | 19 | (defmethod animals-read :default 20 | [{:keys [state] :as env} key params] 21 | (let [st @state] 22 | (if-let [[_ value] (find st key)] 23 | {:value value} 24 | {:value :not-found}))) 25 | 26 | (defmethod animals-read :animals/list 27 | [{:keys [state] :as env} key {:keys [start end]}] 28 | {:value (subvec (:animals/list @state) start end)}) 29 | 30 | (defui AnimalsList 31 | static om/IQueryParams 32 | (params [this] 33 | {:start 0 :end 10}) 34 | static om/IQuery 35 | (query [this] 36 | '[:app/title (:animals/list {:start ?start :end ?end})]) 37 | Object 38 | (render [this] 39 | (let [{:keys [app/title animals/list]} (om/props this)] 40 | (dom/div nil 41 | (dom/h2 nil title) 42 | (apply dom/ul nil 43 | (map 44 | (fn [[i name]] 45 | (dom/li nil (str i ". " name))) 46 | list)))))) 47 | 48 | (def animals-reconciler 49 | (om/reconciler 50 | {:state animals-app-state 51 | :parser (om/parser {:read animals-read})})) 52 | 53 | (defcard animals 54 | (dom-node 55 | (fn [_ node] 56 | (om/add-root! animals-reconciler AnimalsList node)))) 57 | 58 | ;; ============================================================================= 59 | ;; Componentes, Identity & Normalization 60 | 61 | (enable-console-print!) 62 | 63 | (def cian-init-data 64 | {:list/one [{:name "John" :points 0} 65 | {:name "Mary" :points 0} 66 | {:name "Bob" :points 0}] 67 | :list/two [{:name "Mary" :points 0 :age 27} 68 | {:name "Gwen" :points 0} 69 | {:name "Jeff" :points 0}]}) 70 | 71 | ;; ----------------------------------------------------------------------------- 72 | ;; Parsing 73 | 74 | (defmulti cian-read om/dispatch) 75 | 76 | (defn get-people [state key] 77 | (let [st @state] 78 | (into [] (map #(get-in st %)) (get st key)))) 79 | 80 | (defmethod cian-read :list/one 81 | [{:keys [state] :as env} key params] 82 | {:value (get-people state key)}) 83 | 84 | (defmethod cian-read :list/two 85 | [{:keys [state] :as env} key params] 86 | {:value (get-people state key)}) 87 | 88 | (defmulti cian-mutate om/dispatch) 89 | 90 | (defmethod cian-mutate 'points/increment 91 | [{:keys [state]} _ {:keys [name]}] 92 | {:action 93 | (fn [] 94 | (swap! state update-in 95 | [:person/by-name name :points] 96 | inc))}) 97 | 98 | (defmethod cian-mutate 'points/decrement 99 | [{:keys [state]} _ {:keys [name]}] 100 | {:action 101 | (fn [] 102 | (swap! state update-in 103 | [:person/by-name name :points] 104 | #(let [n (dec %)] (if (neg? n) 0 n))))}) 105 | 106 | ;; ----------------------------------------------------------------------------- 107 | ;; Components 108 | 109 | (defui Person 110 | static om/Ident 111 | (ident [this {:keys [name]}] 112 | [:person/by-name name]) 113 | static om/IQuery 114 | (query [this] 115 | '[:name :points :age]) 116 | Object 117 | (render [this] 118 | (println "Render Person" (-> this om/props :name)) 119 | (let [{:keys [points name foo] :as props} (om/props this)] 120 | (dom/li nil 121 | (dom/label nil (str name ", points: " points)) 122 | (dom/button 123 | #js {:onClick 124 | (fn [e] 125 | (om/transact! this 126 | `[(points/increment ~props)]))} 127 | "+") 128 | (dom/button 129 | #js {:onClick 130 | (fn [e] 131 | (om/transact! this 132 | `[(points/decrement ~props)]))} 133 | "-"))))) 134 | 135 | (def person (om/factory Person {:keyfn :name})) 136 | 137 | (defui ListView 138 | Object 139 | (render [this] 140 | (println "Render ListView" (-> this om/path first)) 141 | (let [list (om/props this)] 142 | (apply dom/ul nil 143 | (map person list))))) 144 | 145 | (def list-view (om/factory ListView)) 146 | 147 | (defui RootView 148 | static om/IQuery 149 | (query [this] 150 | (let [subquery (om/get-query Person)] 151 | `[{:list/one ~subquery} {:list/two ~subquery}])) 152 | Object 153 | (render [this] 154 | (println "Render RootView") 155 | (let [{:keys [list/one list/two]} (om/props this)] 156 | (apply dom/div nil 157 | [(dom/h2 nil "List A") 158 | (list-view one) 159 | (dom/h2 nil "List B") 160 | (list-view two)])))) 161 | 162 | (def cian-reconciler 163 | (om/reconciler 164 | {:state cian-init-data 165 | :parser (om/parser {:read cian-read :mutate cian-mutate})})) 166 | 167 | (defcard component-identity-and-normalization 168 | (dom-node 169 | (fn [_ node] 170 | (om/add-root! cian-reconciler RootView node)))) 171 | 172 | ;; ============================================================================= 173 | ;; Thinking With Links! 174 | 175 | (def links-init-data 176 | {:current-user {:email "bob.smith@gmail.com"} 177 | :items [{:id 0 :title "Foo"} 178 | {:id 1 :title "Bar"} 179 | {:id 2 :title "Baz"}]}) 180 | 181 | (defmulti links-read om/dispatch) 182 | 183 | (defmethod links-read :items 184 | [{:keys [query state]} k _] 185 | (let [st @state] 186 | {:value (om/db->tree query (get st k) st)})) 187 | 188 | (defui LinksItem 189 | static om/Ident 190 | (ident [_ {:keys [id]}] 191 | [:item/by-id id]) 192 | static om/IQuery 193 | (query [_] 194 | '[:id :title [:current-user _]]) 195 | Object 196 | (render [this] 197 | (let [{:keys [title current-user]} (om/props this)] 198 | (dom/li nil 199 | (dom/div nil title) 200 | (dom/div nil (:email current-user)))))) 201 | 202 | (def links-item (om/factory LinksItem)) 203 | 204 | (defui LinksSomeList 205 | static om/IQuery 206 | (query [_] 207 | [{:items (om/get-query LinksItem)}]) 208 | Object 209 | (render [this] 210 | (dom/div nil 211 | (dom/h2 nil "A List!") 212 | (dom/ul nil 213 | (map links-item (-> this om/props :items)))))) 214 | 215 | (def links-reconciler 216 | (om/reconciler 217 | {:state links-init-data 218 | :parser (om/parser {:read links-read})})) 219 | 220 | (defcard links 221 | (dom-node 222 | (fn [_ node] 223 | (om/add-root! links-reconciler LinksSomeList node)))) 224 | 225 | ;; ============================================================================= 226 | ;; Queries with unions 227 | 228 | (def union-init-data 229 | {:dashboard/items 230 | [{:id 0 :type :dashboard/post 231 | :author "Laura Smith" 232 | :title "A Post!" 233 | :content "Lorem ipsum dolor sit amet, quem atomorum te quo" 234 | :favorites 0} 235 | {:id 1 :type :dashboard/photo 236 | :title "A Photo!" 237 | :image "photo.jpg" 238 | :caption "Lorem ipsum" 239 | :favorites 0} 240 | {:id 2 :type :dashboard/post 241 | :author "Jim Jacobs" 242 | :title "Another Post!" 243 | :content "Lorem ipsum dolor sit amet, quem atomorum te quo" 244 | :favorites 0} 245 | {:id 3 :type :dashboard/graphic 246 | :title "Charts and Stufff!" 247 | :image "chart.jpg" 248 | :favorites 0} 249 | {:id 4 :type :dashboard/post 250 | :author "May Fields" 251 | :title "Yet Another Post!" 252 | :content "Lorem ipsum dolor sit amet, quem atomorum te quo" 253 | :favorites 0}]}) 254 | 255 | (defui Post 256 | static om/IQuery 257 | (query [this] 258 | [:id :type :title :author :content]) 259 | Object 260 | (render [this] 261 | (let [{:keys [title author content] :as props} (om/props this)] 262 | (dom/div nil 263 | (dom/h3 nil title) 264 | (dom/h4 nil author) 265 | (dom/p nil content))))) 266 | 267 | (def post (om/factory Post)) 268 | 269 | (defui Photo 270 | static om/IQuery 271 | (query [this] 272 | [:id :type :title :image :caption]) 273 | Object 274 | (render [this] 275 | (let [{:keys [title image caption]} (om/props this)] 276 | (dom/div nil 277 | (dom/h3 nil (str "Photo: " title)) 278 | (dom/div nil image) 279 | (dom/p nil "Caption: "))))) 280 | 281 | (def photo (om/factory Photo)) 282 | 283 | (defui Graphic 284 | static om/IQuery 285 | (query [this] 286 | [:id :type :title :image]) 287 | Object 288 | (render [this] 289 | (let [{:keys [title image]} (om/props this)] 290 | (dom/div nil 291 | (dom/h3 nil (str "Graphic: " title)) 292 | (dom/div nil image))))) 293 | 294 | (def graphic (om/factory Graphic)) 295 | 296 | (defui DashboardItem 297 | static om/Ident 298 | (ident [this {:keys [id type]}] 299 | [type id]) 300 | static om/IQuery 301 | (query [this] 302 | (zipmap 303 | [:dashboard/post :dashboard/photo :dashboard/graphic] 304 | (map #(conj % :favorites) 305 | [(om/get-query Post) 306 | (om/get-query Photo) 307 | (om/get-query Graphic)]))) 308 | Object 309 | (render [this] 310 | (let [{:keys [id type favorites] :as props} (om/props this)] 311 | (dom/li 312 | #js {:style #js {:padding 10 :borderBottom "1px solid black"}} 313 | (dom/div nil 314 | (({:dashboard/post post 315 | :dashboard/photo photo 316 | :dashboard/graphic graphic} type) 317 | (om/props this))) 318 | (dom/div nil 319 | (dom/p nil (str "Favorites: " favorites)) 320 | (dom/button 321 | #js {:onClick 322 | (fn [e] 323 | (om/transact! this 324 | `[(dashboard/favorite {:ref [~type ~id]})]))} 325 | "Favorite!")))))) 326 | 327 | (def dashboard-item (om/factory DashboardItem)) 328 | 329 | (defui Dashboard 330 | static om/IQuery 331 | (query [this] 332 | [{:dashboard/items (om/get-query DashboardItem)}]) 333 | Object 334 | (render [this] 335 | (let [{:keys [dashboard/items]} (om/props this)] 336 | (apply dom/ul 337 | #js {:style #js {:padding 0}} 338 | (map dashboard-item items))))) 339 | 340 | (defmulti union-read om/dispatch) 341 | 342 | (defmethod union-read :dashboard/items 343 | [{:keys [state]} k _] 344 | (let [st @state] 345 | {:value (into [] (map #(get-in st %)) (get st k))})) 346 | 347 | (defmulti mutate om/dispatch) 348 | 349 | (defmethod mutate 'dashboard/favorite 350 | [{:keys [state]} k {:keys [ref]}] 351 | {:action 352 | (fn [] 353 | (swap! state update-in (conj ref :favorites) inc))}) 354 | 355 | (def union-reconciler 356 | (om/reconciler 357 | {:state union-init-data 358 | :parser (om/parser {:read union-read :mutate mutate})})) 359 | 360 | (defcard union 361 | (dom-node 362 | (fn [_ node] 363 | (om/add-root! union-reconciler Dashboard node)))) 364 | -------------------------------------------------------------------------------- /src/devcards/om/devcards/utils.cljs: -------------------------------------------------------------------------------- 1 | (ns om.devcards.utils 2 | (:require-macros [cljs.core.async.macros :refer [go]]) 3 | (:require [cljs.core.async :as async :refer [<! >! put! chan]])) 4 | 5 | -------------------------------------------------------------------------------- /src/main/data_readers.clj: -------------------------------------------------------------------------------- 1 | {js clojure.core/identity} 2 | -------------------------------------------------------------------------------- /src/main/deps.cljs: -------------------------------------------------------------------------------- 1 | {:externs ["om/externs.js"]} 2 | -------------------------------------------------------------------------------- /src/main/om/checksums.clj: -------------------------------------------------------------------------------- 1 | (ns om.checksums 2 | (:require [clojure.string :as str])) 3 | 4 | ;; =================================================================== 5 | ;; Checksums (data-react-checksum) 6 | 7 | (def MOD 65521) 8 | 9 | ;; Adapted from https://github.com/tonsky/rum 10 | (defn adler32 [^StringBuilder sb] 11 | (let [l (.length sb) 12 | m (bit-and l -4)] 13 | (loop [a (int 1) 14 | b (int 0) 15 | i 0 16 | n (min (+ i 4096) m)] 17 | (cond 18 | (< i n) 19 | (let [c0 (int (.charAt sb i)) 20 | c1 (int (.charAt sb (+ i 1))) 21 | c2 (int (.charAt sb (+ i 2))) 22 | c3 (int (.charAt sb (+ i 3))) 23 | b (+ b a c0 24 | a c0 c1 25 | a c0 c1 c2 26 | a c0 c1 c2 c3) 27 | a (+ a c0 c1 c2 c3)] 28 | (recur (rem a MOD) (rem b MOD) (+ i 4) n)) 29 | 30 | (< i m) 31 | (recur a b i (min (+ i 4096) m)) 32 | 33 | (< i l) 34 | (let [c0 (int (.charAt sb i))] 35 | (recur (+ a c0) (+ b a c0) (+ i 1) n)) 36 | 37 | :else 38 | (let [a (rem a MOD) 39 | b (rem b MOD)] 40 | (bit-or (int a) (unchecked-int (bit-shift-left b 16)))))))) 41 | 42 | (defn assign-react-checksum [^StringBuilder sb] 43 | (.insert sb (.indexOf sb ">") (str " data-react-checksum=\"" (adler32 sb) "\""))) 44 | -------------------------------------------------------------------------------- /src/main/om/core.clj: -------------------------------------------------------------------------------- 1 | (ns om.core) 2 | 3 | (defmacro component 4 | "Sugar over reify for quickly putting together components that 5 | only need to implement om.core/IRender and don't need access to 6 | the owner argument." 7 | [& body] 8 | `(reify 9 | om.core/IRender 10 | (~'render [this#] 11 | ~@body))) 12 | 13 | -------------------------------------------------------------------------------- /src/main/om/dom.cljs: -------------------------------------------------------------------------------- 1 | (ns om.dom 2 | (:refer-clojure :exclude [map mask meta time select]) 3 | (:require-macros [om.dom :as dom]) 4 | (:require [cljsjs.react] 5 | [cljsjs.react.dom] 6 | [om.util :as util] 7 | [goog.object :as gobj])) 8 | 9 | (dom/gen-react-dom-fns) 10 | 11 | (defn- update-state 12 | "Updates the state of the wrapped input element." 13 | [component next-props value] 14 | (let [on-change (gobj/getValueByKeys component "state" "onChange") 15 | next-state #js {}] 16 | (gobj/extend next-state next-props #js {:onChange on-change}) 17 | (gobj/set next-state "value" value) 18 | (.setState component next-state))) 19 | 20 | (defn wrap-form-element [element] 21 | (let [ctor (fn [props] 22 | (this-as this 23 | (set! (.-state this) 24 | (let [state #js {}] 25 | (->> #js {:onChange (goog/bind (gobj/get this "onChange") this)} 26 | (gobj/extend state props)) 27 | state)) 28 | (.apply js/React.Component this (js-arguments))))] 29 | (set! (.-displayName ctor) (str "wrapped-" element)) 30 | (goog.inherits ctor js/React.Component) 31 | (specify! (.-prototype ctor) 32 | Object 33 | (onChange [this event] 34 | (when-let [handler (.-onChange (.-props this))] 35 | (handler event) 36 | (update-state 37 | this (.-props this) 38 | (gobj/getValueByKeys event "target" "value")))) 39 | 40 | (componentWillReceiveProps [this new-props] 41 | (let [state-value (gobj/getValueByKeys this "state" "value") 42 | element-value (gobj/get (js/ReactDOM.findDOMNode this) "value")] 43 | ;; On IE, onChange event might come after actual value of 44 | ;; an element have changed. We detect this and render 45 | ;; element as-is, hoping that next onChange will 46 | ;; eventually come and bring our modifications anyways. 47 | ;; Ignoring this causes skipped letters in controlled 48 | ;; components 49 | ;; https://github.com/facebook/react/issues/7027 50 | ;; https://github.com/reagent-project/reagent/issues/253 51 | ;; https://github.com/tonsky/rum/issues/86 52 | ;; TODO: Find a better solution, since this conflicts 53 | ;; with controlled/uncontrolled inputs. 54 | ;; https://github.com/r0man/sablono/issues/148 55 | (if (not= state-value element-value) 56 | (update-state this new-props element-value) 57 | (update-state this new-props (gobj/get new-props "value"))))) 58 | 59 | (render [this] 60 | (js/React.createElement element (.-state this)))) 61 | (js/React.createFactory ctor))) 62 | 63 | (def input (wrap-form-element "input")) 64 | 65 | (def textarea (wrap-form-element "textarea")) 66 | 67 | (def option (wrap-form-element "option")) 68 | 69 | (def select (wrap-form-element "select")) 70 | 71 | (defn render 72 | "Equivalent to React.render" 73 | [component el] 74 | (js/ReactDOM.render component el)) 75 | 76 | (defn render-to-str 77 | "Equivalent to React.renderToString" 78 | [c] 79 | (js/ReactDOMServer.renderToString c)) 80 | 81 | (defn node 82 | "Returns the dom node associated with a component's React ref." 83 | ([component] 84 | (js/ReactDOM.findDOMNode component)) 85 | ([component name] 86 | (some-> (.-refs component) (gobj/get name) (js/ReactDOM.findDOMNode)))) 87 | 88 | (defn create-element 89 | "Create a DOM element for which there exists no corresponding function. 90 | Useful to create DOM elements not included in React.DOM. Equivalent 91 | to calling `js/React.createElement`" 92 | ([tag] 93 | (create-element tag nil)) 94 | ([tag opts] 95 | (js/React.createElement tag opts)) 96 | ([tag opts & children] 97 | (js/React.createElement tag opts children))) 98 | -------------------------------------------------------------------------------- /src/main/om/externs.js: -------------------------------------------------------------------------------- 1 | var _reactInternalFiber; 2 | var stateNode; 3 | -------------------------------------------------------------------------------- /src/main/om/impl.cljs: -------------------------------------------------------------------------------- 1 | (ns om.impl) 2 | 3 | (defn get-props 4 | [x] 5 | (aget (.-props x) "__om_cursor")) 6 | -------------------------------------------------------------------------------- /src/main/om/next/cache.cljs: -------------------------------------------------------------------------------- 1 | (ns om.next.cache) 2 | 3 | (deftype Cache [arr index size] 4 | Object 5 | (add [this id x] 6 | (let [x' (vary-meta x assoc :client-time (js/Date.))] 7 | (if (<= size (alength arr)) 8 | (let [id' (.shift arr)] 9 | (swap! index #(-> % (dissoc id') (assoc id x')))) 10 | (swap! index assoc id x'))) 11 | (.push arr id)) 12 | (get [this id] 13 | (get @index id))) 14 | 15 | (defn cache [size] 16 | (Cache. #js [] (atom {}) size)) 17 | -------------------------------------------------------------------------------- /src/main/om/next/impl/parser.cljc: -------------------------------------------------------------------------------- 1 | (ns 2 | ^{:doc " 3 | Generic query expression parsing and AST manipulation. 4 | 5 | QUERY EXPRESSIONS 6 | 7 | Query expressions are a variation on Datomic Pull Syntax 8 | http://docs.datomic.com/pull.html more suitable for generic client/server 9 | state transfer. It's important to note the Om Next query expression syntax is 10 | *not* a strict superset of Datomic Pull. 11 | 12 | A query expression is composed of EDN values. The grammar for query 13 | expressions follows: 14 | 15 | QueryRoot := EdnVector(QueryExpr*) 16 | PlainQueryExpr := (EdnKeyword | IdentExpr | JoinExpr) 17 | QueryExpr := (PlainQueryExpr | ParamExpr) 18 | IdentExpr := EdnVector2(Keyword, EdnValue) 19 | ParamExpr := EdnList2(PlainQueryExpr | EdnSymbol, ParamMapExpr) 20 | ParamMapExpr := EdnMap(Keyword, EdnValue) 21 | JoinExpr := EdnMap((Keyword | IdentExpr), (QueryRoot | UnionExpr | RecurExpr)) 22 | UnionExpr := EdnMap(Keyword, QueryRoot) 23 | RecurExpr := ('... | Integer) 24 | 25 | Note most apis in Om Next expect a QueryRoot not a QueryExpr. 26 | 27 | QUERY EXPRESSION AST FORMAT 28 | 29 | Given a QueryExpr you can get the AST via om.next.impl.parser/expr->ast. 30 | The following keys can appear in the AST representation: 31 | 32 | {:type (:prop | :join | :call | :root | :union | :union-entry) 33 | :key (EdnKeyword | EdnSymbol | IdentExpr) 34 | :dispatch-key (EdnKeyword | EdnSymbol) 35 | :union-key EdnKeyword 36 | :query (QueryRoot | RecurExpr) 37 | :params ParamMapExpr 38 | :children EdnVector(AST) 39 | :component Object 40 | :target EdnKeyword} 41 | 42 | :query and :params may or may not appear. :type :call is only for 43 | mutations."} 44 | om.next.impl.parser 45 | (:require [clojure.set :as set] 46 | [om.util :as util])) 47 | 48 | (declare expr->ast) 49 | 50 | (defn symbol->ast [k] 51 | {:dispatch-key k 52 | :key k}) 53 | 54 | (defn keyword->ast [k] 55 | {:type :prop 56 | :dispatch-key k 57 | :key k}) 58 | 59 | (defn union-entry->ast [[k v]] 60 | (let [component (-> v meta :component)] 61 | (merge 62 | {:type :union-entry 63 | :union-key k 64 | :query v 65 | :children (into [] (map expr->ast) v)} 66 | (when-not (nil? component) 67 | {:component component})))) 68 | 69 | (defn union->ast [m] 70 | {:type :union 71 | :query m 72 | :children (into [] (map union-entry->ast) m)}) 73 | 74 | (defn call->ast [[f args :as call]] 75 | (if (= 'quote f) 76 | (assoc (expr->ast args) :target (or (-> call meta :target) :remote)) 77 | (let [ast (update-in (expr->ast f) [:params] merge (or args {}))] 78 | (cond-> ast 79 | (symbol? (:dispatch-key ast)) (assoc :type :call))))) 80 | 81 | (defn query->ast 82 | "Convert a query to its AST representation." 83 | [query] 84 | (let [component (-> query meta :component)] 85 | (merge 86 | {:type :root 87 | :children (into [] (map expr->ast) query)} 88 | (when-not (nil? component) 89 | {:component component})))) 90 | 91 | (defn join->ast [join] 92 | (let [query-root? (-> join meta :query-root) 93 | [k v] (first join) 94 | ast (expr->ast k) 95 | type (if (= :call (:type ast)) :call :join) 96 | component (-> v meta :component)] 97 | (merge ast 98 | {:type type :query v} 99 | (when-not (nil? component) 100 | {:component component}) 101 | (when query-root? 102 | {:query-root true}) 103 | (when-not (or (number? v) (= '... v)) 104 | (cond 105 | (vector? v) {:children (into [] (map expr->ast) v)} 106 | (map? v) {:children [(union->ast v)]} 107 | :else (throw 108 | (ex-info (str "Invalid join, " join) 109 | {:type :error/invalid-join}))))))) 110 | 111 | (defn ident->ast [[k id :as ref]] 112 | {:type :prop 113 | :dispatch-key k 114 | :key ref}) 115 | 116 | (defn expr->ast 117 | "Given a query expression convert it into an AST." 118 | [x] 119 | (cond 120 | (symbol? x) (symbol->ast x) 121 | (keyword? x) (keyword->ast x) 122 | (map? x) (join->ast x) 123 | (vector? x) (ident->ast x) 124 | (seq? x) (call->ast x) 125 | :else (throw 126 | (ex-info (str "Invalid expression " x) 127 | {:type :error/invalid-expression})))) 128 | 129 | (defn wrap-expr [root? expr] 130 | (if root? 131 | (with-meta 132 | (cond-> expr (keyword? expr) list) 133 | {:query-root true}) 134 | expr)) 135 | 136 | (defn parameterize [expr params] 137 | (if-not (empty? params) 138 | (list expr params) 139 | (list expr))) 140 | 141 | (defn ast->expr 142 | "Given a query expression AST convert it back into a query expression." 143 | ([ast] 144 | (ast->expr ast false)) 145 | ([{:keys [type component] :as ast} unparse?] 146 | (if (= :root type) 147 | (cond-> (into [] (map #(ast->expr % unparse?)) (:children ast)) 148 | (not (nil? component)) (with-meta {:component component})) 149 | (let [{:keys [key query query-root params]} ast] 150 | (wrap-expr query-root 151 | (if (and params (not= :call type)) 152 | (let [expr (ast->expr (dissoc ast :params) unparse?)] 153 | (parameterize expr params)) 154 | (let [key (if (= :call type) (parameterize key params) key)] 155 | (if (or (= :join type) 156 | (and (= :call type) (:children ast))) 157 | (if (and (not= '... query) (not (number? query)) 158 | (or (true? unparse?) 159 | (= :call type))) 160 | (let [{:keys [children]} ast] 161 | (if (and (== 1 (count children)) 162 | (= :union (:type (first children)))) ;; UNION 163 | {key (into (cond-> {} 164 | component (with-meta {:component component})) 165 | (map (fn [{:keys [union-key children component]}] 166 | [union-key 167 | (cond-> (into [] (map #(ast->expr % unparse?)) children) 168 | (not (nil? component)) (with-meta {:component component}))])) 169 | (:children (first children)))} 170 | {key (cond-> (into [] (map #(ast->expr % unparse?)) children) 171 | (not (nil? component)) (with-meta {:component component}))})) 172 | {key query}) 173 | key)))))))) 174 | 175 | (defn path-meta 176 | "Add path metadata to a data structure. data is the data to be worked on. 177 | path is the current path into the data. query is the query used to 178 | walk the data. union-expr tracks the last seen union query to be used 179 | when it finds a recursive union." 180 | ([data path query] 181 | (path-meta data path query nil)) 182 | ([data path query union-expr] 183 | (cond 184 | (nil? query) 185 | (cond-> data 186 | #?(:clj (instance? clojure.lang.IObj data) 187 | :cljs (satisfies? IWithMeta data)) 188 | (vary-meta assoc :om-path path)) 189 | 190 | (sequential? data) 191 | (-> (into [] 192 | (map-indexed 193 | (fn [idx v] 194 | (path-meta v (conj path idx) query union-expr))) data) 195 | (vary-meta assoc :om-path path)) 196 | 197 | (vector? query) 198 | (loop [joins (seq query) ret data] 199 | (if-not (nil? joins) 200 | (let [join (first joins)] 201 | (if-not (or (util/join? join) 202 | (util/ident? join) 203 | (and (seq? join) 204 | (util/ident? (first join)))) 205 | (recur (next joins) ret) 206 | (let [join (cond-> join (seq? join) first) 207 | join (cond-> join (util/ident? join) (hash-map '[*])) 208 | [key sel] (util/join-entry join) 209 | union-entry (if (util/union? join) sel union-expr) 210 | sel (if (util/recursion? sel) 211 | (if-not (nil? union-expr) 212 | union-entry 213 | query) 214 | sel) 215 | key (cond-> key (util/unique-ident? key) first) 216 | v (get ret key)] 217 | (recur (next joins) 218 | (cond-> ret 219 | (and (map? ret) (contains? ret key)) 220 | (assoc key 221 | (path-meta v (conj path key) sel union-entry))))))) 222 | (cond-> ret 223 | #?(:clj (instance? clojure.lang.IObj ret) 224 | :cljs (satisfies? IWithMeta ret)) 225 | (vary-meta assoc :om-path path)))) 226 | 227 | :else 228 | ;; UNION 229 | (if (map? data) 230 | (let [dispatch-key (comp :dispatch-key expr->ast) 231 | branches (vals query) 232 | props (map dispatch-key (keys data)) 233 | query (reduce (fn [ret q] 234 | (let [query-props (into #{} (map dispatch-key) q) 235 | props (set props)] 236 | (cond 237 | (= (set props) 238 | (set query-props)) (reduced q) 239 | (set/subset? props query-props) q 240 | :else ret))) 241 | nil branches)] 242 | (path-meta data path query union-expr)) 243 | data)))) 244 | 245 | (defn rethrow? [x] 246 | (and (instance? #?(:clj clojure.lang.ExceptionInfo :cljs ExceptionInfo) x) 247 | (= :om.next/abort (-> x ex-data :type)))) 248 | 249 | (defn parser 250 | "Given a :read and/or :mutate function return a parser. Refer to om.next/parser 251 | for top level documentation." 252 | [{:keys [read mutate] :as config}] 253 | (fn self 254 | ([env query] (self env query nil)) 255 | ([env query target] 256 | (let [elide-paths? (or (:elide-paths config) (:query-root env)) 257 | {:keys [path] :as env} 258 | (cond-> (assoc env :parser self :target target :query-root :om.next/root) 259 | (not (contains? env :path)) (assoc :path []))] 260 | (letfn [(step [ret expr] 261 | (let [{query' :query :keys [key dispatch-key params] :as ast} (expr->ast expr) 262 | env (cond-> (merge env {:ast ast :query query'}) 263 | (nil? query') (dissoc :query) 264 | (= '... query') (assoc :query query) 265 | (vector? key) (assoc :query-root key)) 266 | type (:type ast) 267 | call? (= :call type) 268 | res (case type 269 | :call 270 | (do 271 | (assert mutate "Parse mutation attempted but no :mutate function supplied") 272 | (mutate env dispatch-key params)) 273 | (:prop :join :union) 274 | (do 275 | (assert read "Parse read attempted but no :read function supplied") 276 | (read env dispatch-key params)))] 277 | (if-not (nil? target) 278 | (let [ast' (get res target)] 279 | (cond-> ret 280 | (true? ast') (conj expr) 281 | (map? ast') (conj (ast->expr ast')))) 282 | (if-not (or call? (nil? (:target ast)) (contains? res :value)) 283 | ret 284 | (let [error (atom nil) 285 | mut-ret (atom nil)] 286 | (when (and call? (not (nil? (:action res)))) 287 | (try 288 | (reset! mut-ret ((:action res))) 289 | (catch #?(:clj Throwable :cljs :default) e 290 | (if (rethrow? e) 291 | (throw e) 292 | (reset! error e))))) 293 | (let [value (:value res)] 294 | (when call? 295 | (assert (or (nil? value) (map? value)) 296 | (str dispatch-key " mutation :value must be nil or a map with structure {:keys [...]}"))) 297 | (cond-> ret 298 | (not (nil? value)) (assoc (cond-> key 299 | (util/unique-ident? key) 300 | first) 301 | value) 302 | @mut-ret (assoc-in [key :result] @mut-ret) 303 | @error (assoc key {:om.next/error @error}))))))))] 304 | (cond-> (reduce step (if (nil? target) {} []) query) 305 | (and (nil? target) (not elide-paths?)) (path-meta path query))))))) 306 | 307 | (defn dispatch [_ k _] k) 308 | -------------------------------------------------------------------------------- /src/main/om/next/protocols.cljc: -------------------------------------------------------------------------------- 1 | (ns om.next.protocols) 2 | 3 | (defprotocol IIndexer 4 | (indexes [this]) 5 | (index-root [this x]) 6 | (index-component! [this component]) 7 | (drop-component! [this component]) 8 | (ref-for [this component]) 9 | (key->components [this k])) 10 | 11 | (defprotocol IReconciler 12 | (basis-t [this]) 13 | (add-root! [reconciler root-class target options]) 14 | (remove-root! [reconciler target]) 15 | (schedule-render! [reconciler]) 16 | (schedule-sends! [reconciler]) 17 | (queue! [reconciler ks] [reconciler ks remote]) 18 | (queue-sends! [reconciler sends]) 19 | (reindex! [reconciler]) 20 | (reconcile! [reconciler] [reconciler remote]) 21 | (send! [reconciler])) 22 | 23 | #?(:clj 24 | (defprotocol IReactDOMElement 25 | (^String -render-to-string [this react-id ^StringBuilder sb] "renders a DOM node to string."))) 26 | 27 | #?(:clj 28 | (defprotocol IReactComponent 29 | (-render [this] "must return a valid ReactDOMElement."))) 30 | 31 | #?(:clj 32 | (defprotocol IReactLifecycle 33 | (shouldComponentUpdate [this next-props next-state]) 34 | (initLocalState [this]) 35 | (componentWillReceiveProps [this next-props]) 36 | (componentWillUpdate [this next-props next-state]) 37 | (componentDidUpdate [this prev-props prev-state]) 38 | (componentWillMount [this]) 39 | (componentDidMount [this]) 40 | (componentWillUnmount [this]) 41 | (render [this]))) 42 | -------------------------------------------------------------------------------- /src/main/om/next/server.clj: -------------------------------------------------------------------------------- 1 | (ns om.next.server 2 | (:require [om.next.impl.parser :as parser] 3 | [om.transit :as transit])) 4 | 5 | (defn parser 6 | "Create a parser. The argument is a map of two keys, :read and :mutate. Both 7 | functions should have the signature (Env -> Key -> Params -> ParseResult)." 8 | [opts] 9 | (parser/parser (assoc opts :elide-paths true))) 10 | 11 | (defn dispatch 12 | "Helper function for implementing :read and :mutate as multimethods. Use this 13 | as the dispatch-fn." 14 | [_ key _] key) 15 | 16 | (defn reader 17 | "Create a Om Next transit reader. This reader can handler the tempid type. 18 | Can pass transit reader customization opts map." 19 | ([in] (transit/reader in)) 20 | ([in opts] (transit/reader in opts))) 21 | 22 | (defn writer 23 | "Create a Om Next transit reader. This writer can handler the tempid type. 24 | Can pass transit writer customization opts map." 25 | ([out] (transit/writer out)) 26 | ([out opts] (transit/writer out opts))) 27 | -------------------------------------------------------------------------------- /src/main/om/tempid.cljc: -------------------------------------------------------------------------------- 1 | (ns om.tempid 2 | #?(:clj (:import [java.io Writer]))) 3 | 4 | ;; ============================================================================= 5 | ;; ClojureScript 6 | 7 | #?(:cljs 8 | (deftype TempId [^:mutable id ^:mutable __hash] 9 | Object 10 | (toString [this] 11 | (pr-str this)) 12 | IEquiv 13 | (-equiv [this other] 14 | (and (instance? TempId other) 15 | (= (. this -id) (. other -id)))) 16 | IHash 17 | (-hash [this] 18 | (when (nil? __hash) 19 | (set! __hash (hash id))) 20 | __hash) 21 | IPrintWithWriter 22 | (-pr-writer [_ writer _] 23 | (write-all writer "#om/id[\"" id "\"]")))) 24 | 25 | #?(:cljs 26 | (defn tempid 27 | ([] 28 | (tempid (random-uuid))) 29 | ([id] 30 | (TempId. id nil)))) 31 | 32 | ;; ============================================================================= 33 | ;; Clojure 34 | 35 | #?(:clj 36 | (defrecord TempId [id] 37 | Object 38 | (toString [this] 39 | (pr-str this)))) 40 | 41 | #?(:clj 42 | (defmethod print-method TempId [^TempId x ^Writer writer] 43 | (.write writer (str "#om/id[\"" (.id x) "\"]")))) 44 | 45 | #?(:clj 46 | (defn tempid 47 | ([] 48 | (tempid (java.util.UUID/randomUUID))) 49 | ([uuid] 50 | (TempId. uuid)))) 51 | 52 | (defn tempid? 53 | #?(:cljs {:tag boolean}) 54 | [x] 55 | (instance? TempId x)) 56 | -------------------------------------------------------------------------------- /src/main/om/transit.cljc: -------------------------------------------------------------------------------- 1 | (ns om.transit 2 | #?(:clj (:refer-clojure :exclude [ref])) 3 | (:require [cognitect.transit :as t] 4 | #?(:cljs [com.cognitect.transit :as ct]) 5 | [om.tempid :as tempid #?@(:cljs [:refer [TempId]])]) 6 | #?(:clj (:import [com.cognitect.transit 7 | TransitFactory WriteHandler ReadHandler] 8 | [om.tempid TempId]))) 9 | 10 | #?(:cljs 11 | (deftype TempIdHandler [] 12 | Object 13 | (tag [_ _] "om/id") 14 | (rep [_ r] (. r -id)) 15 | (stringRep [_ _] nil))) 16 | 17 | #?(:clj 18 | (deftype TempIdHandler [] 19 | WriteHandler 20 | (tag [_ _] "om/id") 21 | (rep [_ r] (. ^TempId r -id)) 22 | (stringRep [_ r] (. ^TempId r -id)) 23 | (getVerboseHandler [_] nil))) 24 | 25 | #?(:cljs 26 | (defn writer 27 | ([] 28 | (writer {})) 29 | ([opts] 30 | (t/writer :json 31 | (assoc-in opts [:handlers TempId] (TempIdHandler.)))))) 32 | 33 | #?(:clj 34 | (defn writer 35 | ([out] 36 | (writer out {})) 37 | ([out opts] 38 | (t/writer out :json 39 | (assoc-in opts [:handlers TempId] (TempIdHandler.)))))) 40 | 41 | #?(:cljs 42 | (defn reader 43 | ([] 44 | (reader {})) 45 | ([opts] 46 | (t/reader :json 47 | (assoc-in opts 48 | [:handlers "om/id"] 49 | (fn [id] (tempid/tempid id))))))) 50 | 51 | #?(:clj 52 | (defn reader 53 | ([in] 54 | (reader in {})) 55 | ([in opts] 56 | (t/reader in :json 57 | (assoc-in opts 58 | [:handlers "om/id"] 59 | (reify 60 | ReadHandler 61 | (fromRep [_ id] (TempId. id)))))))) 62 | 63 | (comment 64 | ;; cljs 65 | (t/read (reader) (t/write (writer) (tempid/tempid))) 66 | 67 | ;; clj 68 | (import '[java.io ByteArrayOutputStream ByteArrayInputStream]) 69 | 70 | (def baos (ByteArrayOutputStream. 4096)) 71 | (def w (writer baos)) 72 | (t/write w (TempId. (java.util.UUID/randomUUID))) 73 | (.toString baos) 74 | 75 | (def in (ByteArrayInputStream. (.toByteArray baos))) 76 | (def r (reader in)) 77 | (t/read r) 78 | ) -------------------------------------------------------------------------------- /src/main/om/util.cljc: -------------------------------------------------------------------------------- 1 | (ns om.util 2 | (:refer-clojure :exclude [ident?])) 3 | 4 | (defn force-children [x] 5 | (cond->> x 6 | (seq? x) (into [] (map force-children)))) 7 | 8 | (defn union? 9 | #?(:cljs {:tag boolean}) 10 | [expr] 11 | (let [expr (cond-> expr (seq? expr) first)] 12 | (and (map? expr) 13 | (map? (-> expr first second))))) 14 | 15 | (defn join? 16 | #?(:cljs {:tag boolean}) 17 | [x] 18 | (let [x (if (seq? x) (first x) x)] 19 | (map? x))) 20 | 21 | (defn ident? 22 | "Returns true if x is an ident." 23 | #?(:cljs {:tag boolean}) 24 | [x] 25 | (and (vector? x) 26 | (== 2 (count x)) 27 | (keyword? (nth x 0)))) 28 | 29 | (defn join-entry [expr] 30 | (if (seq? expr) 31 | (ffirst expr) 32 | (first expr))) 33 | 34 | (defn join-key [expr] 35 | (cond 36 | (map? expr) (ffirst expr) 37 | (seq? expr) (join-key (first expr)) 38 | :else expr)) 39 | 40 | (defn join-value [join] 41 | (second (join-entry join))) 42 | 43 | (defn unique-ident? 44 | #?(:cljs {:tag boolean}) 45 | [x] 46 | (and (ident? x) (= '_ (second x)))) 47 | 48 | (defn recursion? 49 | #?(:cljs {:tag boolean}) 50 | [x] 51 | (or #?(:clj (= '... x) 52 | :cljs (symbol-identical? '... x)) 53 | (number? x))) 54 | 55 | (defn mutation? 56 | #?(:cljs {:tag boolean}) 57 | [expr] 58 | (let [expr (cond-> expr (seq? expr) first)] 59 | (symbol? expr))) 60 | 61 | (defn mutation-key [expr] 62 | {:pre [(symbol? (first expr))]} 63 | (first expr)) 64 | -------------------------------------------------------------------------------- /src/test/om/checksums_test.clj: -------------------------------------------------------------------------------- 1 | (ns om.checksums-test 2 | (:require [clojure.test :refer [deftest testing is are]] 3 | [om.test-utils :refer [remove-whitespace]] 4 | [om.checksums :as chk])) 5 | 6 | (deftest test-checksum 7 | (are [data res] (= (chk/adler32 (StringBuilder. data)) res) 8 | "<div data-reactid=\".p55bcrvgg0\"></div>" -47641439 9 | "<div data-reactid=\".0\" id=\"foo\">Hello World</div>" -1847259110 10 | (remove-whitespace "<div data-reactid=\".0\"> 11 | <div data-reactid=\".0.0\"> 12 | <button data-reactid=\".0.0.0\">Load children</button> 13 | <ul data-reactid=\".0.0.1\"> 14 | <li data-reactid=\".0.0.1.0\">1</li> 15 | <li data-reactid=\".0.0.1.1\">2</li> 16 | <li data-reactid=\".0.0.1.2\">3</li> 17 | </ul> 18 | </div> 19 | </div>") 821447442 20 | "Lorem ipsum dolor sit amet, ea nam mutat probatus. Erat volutpat liberavisse eu his, 21 | id qui eius congue accumsan. Pro erat natum ponderum ut. Vidit assum eu eam. Mei animal 22 | epicurei facilisi te. In eum euismod principes, id soluta volutpat pri. Nulla harum ex has, 23 | aliquam verterem recteque has eu. Cu noster utamur quaestio quo, eos eius diceret ei. Ponderum 24 | atomorum has et. Mel amet dolores philosophia ut, eam id erat noluisse postulant. Sit vide 25 | regione eu. Ne vis justo liber." -1507348871)) 26 | 27 | (deftest test-assign-react-checksum 28 | (is (= (str (chk/assign-react-checksum (StringBuilder. "<div data-reactid=\".p55bcrvgg0\"></div>"))) 29 | "<div data-reactid=\".p55bcrvgg0\" data-react-checksum=\"-47641439\"></div>")) 30 | (is (= (str (chk/assign-react-checksum (StringBuilder. "<div data-reactid=\".0\" id=\"foo\">Hello World</div>"))) 31 | "<div data-reactid=\".0\" id=\"foo\" data-react-checksum=\"-1847259110\">Hello World</div>"))) 32 | -------------------------------------------------------------------------------- /src/test/om/dom_test.clj: -------------------------------------------------------------------------------- 1 | (ns om.dom-test 2 | (:refer-clojure :exclude [read]) 3 | (:require [clojure.test :refer [deftest testing is are]] 4 | [om.test-utils :refer [remove-whitespace]] 5 | [om.next :as om :refer [defui]] 6 | [om.dom :as dom] 7 | [om.next.protocols :as p])) 8 | 9 | (defn test-tags [tags res-fn] 10 | `(are [element# res#] (let [sb# (StringBuilder.)] 11 | (dom/render-element! {:tag element#} (volatile! 1) sb#) 12 | (= (str sb#) res#)) 13 | ~@(mapcat (fn [tag#] [tag# (res-fn tag#)]) tags))) 14 | 15 | (defmacro test-container-tags [] 16 | (let [container-tags (->> dom/tags 17 | (map str) 18 | (filter #(dom/container-tag? % nil)))] 19 | (test-tags container-tags #(str "<" % " data-reactroot=\"\" data-reactid=\"1\">" "</" % ">")))) 20 | 21 | (defmacro test-void-tags [] 22 | (let [container-tags (->> dom/tags 23 | (map str) 24 | (filter #(not (dom/container-tag? % nil))))] 25 | (test-tags container-tags #(str "<" % " data-reactroot=\"\" data-reactid=\"1\"/>")))) 26 | 27 | (defn simple-component [] 28 | (dom/div nil "Hello World")) 29 | 30 | (defn simple-nested-component [] 31 | (dom/div nil 32 | (dom/h1 #js {:id "page-title"} "Title"))) 33 | 34 | (defn comp-nested-component [] 35 | (dom/div nil 36 | (simple-component) 37 | (simple-nested-component))) 38 | 39 | (deftest test-render-element 40 | (testing "render-element works with empty content in all tags" 41 | (test-container-tags) 42 | (test-void-tags)) 43 | (testing "render-element renders simple function elements" 44 | (are [component res] (let [sb (StringBuilder.)] 45 | (dom/render-element! component (volatile! 1) sb) 46 | (= (str sb) res)) 47 | (simple-component) "<div data-reactroot=\"\" data-reactid=\"1\">Hello World</div>" 48 | (simple-nested-component) (remove-whitespace 49 | "<div data-reactroot=\"\" data-reactid=\"1\"> 50 | <h1 id=\"page-title\" data-reactid=\"2\">Title</h1> 51 | </div>") 52 | (comp-nested-component) (remove-whitespace 53 | "<div data-reactroot=\"\" data-reactid=\"1\"> 54 | <div data-reactid=\"2\">Hello World</div> 55 | <div data-reactid=\"3\"> 56 | <h1 id=\"page-title\" data-reactid=\"4\">Title</h1> 57 | </div> 58 | </div>")))) 59 | 60 | (defui SimpleComponent 61 | Object 62 | (render [this] 63 | (dom/div nil "Hello World"))) 64 | 65 | (defui Hello 66 | Object 67 | (render [this] 68 | (dom/p nil (-> this om/props :text)))) 69 | 70 | (defui Children 71 | Object 72 | (render [this] 73 | (dom/div nil 74 | (map identity 75 | #js [(dom/div nil "Foo") 76 | (dom/div nil "Bar") 77 | (map identity 78 | #js [(dom/div nil "Bar") 79 | (dom/div nil "Woz")])])))) 80 | 81 | (deftest test-render-to-str 82 | (let [c ((om/factory SimpleComponent))] 83 | (is (= (str (#'dom/render-to-str* c)) "<div data-reactroot=\"\" data-reactid=\"1\">Hello World</div>"))) 84 | (let [hello (om/factory Hello)] 85 | (is (= (str (#'dom/render-to-str* (hello {:text "Hello, world!"}))) 86 | "<p data-reactroot=\"\" data-reactid=\"1\">Hello, world!</p>"))) 87 | (let [children (om/factory Children)] 88 | (is (= (str (#'dom/render-to-str* (children))) 89 | (remove-whitespace "<div data-reactroot=\"\" data-reactid=\"1\"> 90 | <div data-reactid=\"2\">Foo</div> 91 | <div data-reactid=\"3\">Bar</div> 92 | <div data-reactid=\"4\">Bar</div> 93 | <div data-reactid=\"5\">Woz</div> 94 | </div>"))))) 95 | 96 | (deftest test-format-react-attrs 97 | (are [map res] (let [sb (StringBuilder.)] 98 | (dom/render-attr-map! sb "div" map) 99 | (= (str sb) res)) 100 | {:htmlFor "something"} " for=\"something\"" 101 | {:className "foo"} " class=\"foo\"" 102 | {:srcLang "en"} " srclang=\"en\"" 103 | {:acceptCharset "ISO-8859-1"} " accept-charset=\"ISO-8859-1\"" 104 | {:placeholder "Title"} " placeholder=\"Title\"" 105 | ;; svg xlink:stuff 106 | {:xlinkActuate "foo"} " xlink:actuate=\"foo\"")) 107 | 108 | (deftest test-ref-is-elided-in-props 109 | (let [sb (StringBuilder.)] 110 | (dom/render-element! (dom/div #js {:ref "someDiv"}) (volatile! 1) sb) 111 | (is (= (str sb) 112 | "<div data-reactroot=\"\" data-reactid=\"1\"></div>")))) 113 | 114 | (deftest test-attrs-rendered-in-declaration-order 115 | (are [element res] (let [sb (StringBuilder.)] 116 | (dom/render-element! element (volatile! 1) sb) 117 | (= (str sb) res)) 118 | (dom/input {:type "text" 119 | :placeholder "some text" 120 | :id "stuff"}) 121 | "<input type=\"text\" placeholder=\"some text\" id=\"stuff\" data-reactroot=\"\" data-reactid=\"1\"/>" 122 | 123 | (dom/input {:id "stuff" 124 | :placeholder "some text" 125 | :type "text"}) 126 | "<input type=\"text\" id=\"stuff\" placeholder=\"some text\" data-reactroot=\"\" data-reactid=\"1\"/>" 127 | 128 | (dom/input {:placeholder "some text" 129 | :id "stuff" 130 | :type "text"}) 131 | "<input type=\"text\" placeholder=\"some text\" id=\"stuff\" data-reactroot=\"\" data-reactid=\"1\"/>")) 132 | 133 | (deftest test-only-supported-attrs-rendered 134 | (are [element markup] (= (str (#'dom/render-to-str* element)) (remove-whitespace markup)) 135 | (dom/div #js {:not-supported "foo"}) "<div data-reactroot=\"\" data-reactid=\"1\"></div>" 136 | (dom/div {:className "stuff" :class "other"}) "<div class=\"stuff\" data-reactroot=\"\" data-reactid=\"1\"></div>" 137 | (dom/div {:media :stuff}) "<div data-reactroot=\"\" data-reactid=\"1\"></div>" 138 | (dom/div {:data-foo "foo"}) "<div data-foo=\"foo\" data-reactroot=\"\" data-reactid=\"1\"></div>" 139 | (dom/div {:foo true}) "<div data-reactroot=\"\" data-reactid=\"1\"></div>" 140 | (dom/div {:autoFocus true}) "<div autofocus data-reactroot=\"\" data-reactid=\"1\"></div>" 141 | (dom/div #js {:autoCapitalize true 142 | :color "tomato" 143 | :itemScope 1 144 | :keyParams true}) 145 | "<div autocapitalize color=\"tomato\" itemscope=\"1\" keyparams data-reactroot=\"\" data-reactid=\"1\"></div>" 146 | (dom/svg #js {:panose1 "stuff"}) "<svg panose-1=\"stuff\" data-reactroot=\"\" data-reactid=\"1\"></svg>")) 147 | 148 | 149 | (def styles 150 | #js {:textAlign "center" 151 | :marginLeft "10px"}) 152 | 153 | (defui ComponentWithStyle 154 | Object 155 | (render [this] 156 | (dom/div #js {:style styles}))) 157 | 158 | (deftest test-normalize-styles 159 | (are [styles result] (let [sb (StringBuilder.)] 160 | (dom/normalize-styles! sb styles) 161 | (= (str sb) result)) 162 | (select-keys styles [:textAlign]) "text-align:center;" 163 | styles "text-align:center;margin-left:10px;" 164 | {:zoom 1} "zoom:1;" 165 | {:zoom 1 166 | :opacity 0.5 167 | :width 100} "zoom:1;opacity:0.5;width:100px;")) 168 | 169 | (deftest test-empty-styles-not-rendered 170 | (let [sb (StringBuilder.)] 171 | (dom/render-element! (dom/div {:style {}}) (volatile! 1) sb) 172 | (is (= (str sb) 173 | "<div data-reactroot=\"\" data-reactid=\"1\"></div>")))) 174 | 175 | (deftest test-render-component-with-style 176 | (let [ctor (om/factory ComponentWithStyle)] 177 | (is (= (str (#'dom/render-to-str* (ctor))) 178 | "<div style=\"text-align:center;margin-left:10px;\" data-reactroot=\"\" data-reactid=\"1\"></div>")))) 179 | 180 | ;; Simple nested `defui`s 181 | 182 | (defui SimpleNestedChild 183 | Object 184 | (render [this] 185 | (dom/div nil "child"))) 186 | 187 | (def simple-nested-child-factory (om/factory SimpleNestedChild)) 188 | 189 | (defui SimpleNestedParent 190 | Object 191 | (render [this] 192 | (dom/div nil 193 | (simple-nested-child-factory)))) 194 | 195 | (deftest test-simple-nested-defuis 196 | (let [ctor (om/factory SimpleNestedParent)] 197 | (is (= (str (#'dom/render-to-str* (ctor))) 198 | (remove-whitespace "<div data-reactroot=\"\" data-reactid=\"1\"> 199 | <div data-reactid=\"2\">child</div> 200 | </div>"))))) 201 | 202 | 203 | ;; Om Simple Recursive Tree 204 | (def simple-tree-data 205 | {:tree {:node-value 1 206 | :children [{:node-value 2 207 | :children [{:node-value 3 208 | :children []}]} 209 | {:node-value 4 210 | :children []}]}}) 211 | 212 | (declare simple-node) 213 | 214 | (defui SimpleNode 215 | static om/IQuery 216 | (query [this] 217 | '[:node-value {:children ...}]) 218 | Object 219 | (render [this] 220 | (let [{:keys [node-value children]} (om/props this)] 221 | (dom/li nil 222 | (dom/div nil (str "Node value:" node-value)) 223 | (dom/ul nil 224 | (map simple-node children)))))) 225 | 226 | (def simple-node (om/factory SimpleNode)) 227 | 228 | (defui SimpleTree 229 | static om/IQuery 230 | (query [this] 231 | [{:tree (om/get-query SimpleNode)}]) 232 | Object 233 | (render [this] 234 | (let [{:keys [tree]} (om/props this)] 235 | (dom/ul nil 236 | (simple-node tree))))) 237 | 238 | (defmulti simple-tree-read om/dispatch) 239 | 240 | (defmethod simple-tree-read :node-value 241 | [{:keys [data] :as env} _ _] 242 | {:value (:node-value data)}) 243 | 244 | (defmethod simple-tree-read :children 245 | [{:keys [data parser query] :as env} _ _] 246 | {:value (let [f #(parser (assoc env :data %) query)] 247 | (into [] (map f (:children data))))}) 248 | 249 | (defmethod simple-tree-read :tree 250 | [{:keys [state parser query] :as env} k _] 251 | (let [st @state] 252 | {:value (parser (assoc env :data (:tree st)) query)})) 253 | 254 | (def simple-tree-reconciler 255 | (om/reconciler 256 | {:state (atom simple-tree-data) 257 | :normalize false 258 | :parser (om/parser {:read simple-tree-read})})) 259 | 260 | (deftest test-render-simple-recursive-example 261 | (let [c (om/add-root! simple-tree-reconciler SimpleTree nil)] 262 | (is (= (str (#'dom/render-to-str* c)) 263 | (remove-whitespace 264 | "<ul data-reactroot=\"\" data-reactid=\"1\"> 265 | <li data-reactid=\"2\"> 266 | <div data-reactid=\"3\">Node value:1</div> 267 | <ul data-reactid=\"4\"> 268 | <li data-reactid=\"5\"> 269 | <div data-reactid=\"6\">Node value:2</div> 270 | <ul data-reactid=\"7\"> 271 | <li data-reactid=\"8\"> 272 | <div data-reactid=\"9\">Node value:3</div> 273 | <ul data-reactid=\"10\"></ul> 274 | </li> 275 | </ul> 276 | </li> 277 | <li data-reactid=\"11\"> 278 | <div data-reactid=\"12\">Node value:4</div> 279 | <ul data-reactid=\"13\"></ul> 280 | </li> 281 | </ul> 282 | </li> 283 | </ul>"))))) 284 | 285 | (defn MultipleTextChildren [] 286 | (dom/div nil 287 | "Some text" 288 | "More text")) 289 | 290 | (defn ChildAndText [] 291 | (dom/div nil 292 | (dom/p nil "A paragraph!") 293 | "More text")) 294 | 295 | (deftest test-render-multiple-text-children 296 | (testing "rendering an element with multiple children converts text nodes to <span>" 297 | (are [comp res] (let [sb (StringBuilder.)] 298 | (dom/render-element! (comp) (volatile! 1) sb) 299 | (= (str sb) 300 | (remove-whitespace res))) 301 | MultipleTextChildren "<div data-reactroot=\"\" data-reactid=\"1\"> 302 | <!-- react-text: 2 -->Some text<!-- /react-text --> 303 | <!-- react-text: 3 -->More text<!-- /react-text --> 304 | </div>" 305 | ChildAndText "<div data-reactroot=\"\" data-reactid=\"1\"> 306 | <p data-reactid=\"2\">A paragraph!</p> 307 | <!-- react-text: 3 -->More text<!-- /react-text --> 308 | </div>"))) 309 | 310 | ;; Shared test 311 | 312 | (defui Home 313 | static om/IQuery 314 | (query [this] [:counter]) 315 | 316 | Object 317 | (render [this] 318 | (let [shared (om/shared this) 319 | props (om/props this)] 320 | (dom/div nil 321 | (dom/h3 nil (str "Props: " props)) 322 | (dom/h3 nil (str "Shared: " shared)) 323 | (dom/button 324 | #js {:onClick #(om/transact! this '[(my/test) :counter])} 325 | "Increment!"))))) 326 | 327 | (def app-state (atom {:counter 0})) 328 | 329 | (defn read 330 | [env key params] 331 | (let [{:keys [state]} env] 332 | {:value (get @state key)})) 333 | 334 | (defn mutate 335 | [env key params] 336 | (let [{:keys [state]} env] 337 | {:value {:keys [:counter]} 338 | :action #(swap! state update-in [:counter] inc)})) 339 | 340 | (def reconciler 341 | (om/reconciler 342 | {:state app-state 343 | :parser (om/parser {:read read :mutate mutate}) 344 | :shared {} 345 | :shared-fn (fn [root-props] 346 | root-props)})) 347 | 348 | (deftest test-shared 349 | (let [c (om/add-root! reconciler Home nil)] 350 | (is (= (str (#'dom/render-to-str* c)) 351 | (remove-whitespace "<div data-reactroot=\"\" data-reactid=\"1\"> 352 | <h3 data-reactid=\"2\">Props: {:counter 0}</h3> 353 | <h3 data-reactid=\"3\">Shared: {:counter 0}</h3> 354 | <button data-reactid=\"4\">Increment!</button> 355 | </div>"))))) 356 | 357 | (deftest test-render-to-str-elements 358 | (are [elem res] (= (str (#'dom/render-to-str* elem)) res) 359 | (dom/div nil "foo") "<div data-reactroot=\"\" data-reactid=\"1\">foo</div>")) 360 | 361 | (deftest react-key-in-elements 362 | (is (= (:react-key (dom/div {:key "foo"})) "foo")) 363 | (is (= (:attrs (dom/div {:key "foo"})) {})) 364 | (is (= (:react-key (dom/div nil)) nil)) 365 | (is (= (str (#'dom/render-to-str* (dom/div {:key "foo"}))) 366 | "<div data-reactroot=\"\" data-reactid=\"1\"></div>")) 367 | (is (= (str (#'dom/render-to-str* (dom/div nil (dom/div #js {:key "foo"})))) 368 | "<div data-reactroot=\"\" data-reactid=\"1\"><div data-reactid=\"2\"></div></div>"))) 369 | 370 | (deftest test-non-string-attributes 371 | (is (= (str (#'dom/render-to-str* (dom/div {:className 3}))) 372 | "<div class=\"3\" data-reactroot=\"\" data-reactid=\"1\"></div>"))) 373 | 374 | (defui NilChild 375 | Object 376 | (render [this] 377 | nil)) 378 | 379 | (def nil-child-factory (om/factory NilChild)) 380 | 381 | (defui NilParent 382 | Object 383 | (render [this] 384 | (dom/div nil 385 | "foo" 386 | (nil-child-factory) 387 | "bar"))) 388 | 389 | (defui NilChildrenComp 390 | Object 391 | (render [this] 392 | (dom/div #js {} 393 | nil))) 394 | 395 | (deftest test-nil-children 396 | (is (= (str (#'dom/render-to-str* (nil-child-factory))) 397 | "<!-- react-empty: 1 -->")) 398 | (is (= (str (#'dom/render-to-str* ((om/factory NilChildrenComp)))) 399 | "<div data-reactroot=\"\" data-reactid=\"1\"></div>")) 400 | (is (= (str (#'dom/render-to-str* ((om/factory NilParent)))) 401 | (remove-whitespace "<div data-reactroot=\"\" data-reactid=\"1\"> 402 | <!-- react-text: 2 -->foo<!-- /react-text --> 403 | <!-- react-empty: 3 --> 404 | <!-- react-text: 4 -->bar<!-- /react-text --></div>")))) 405 | 406 | (defui CLPHN-3-Component-1 407 | Object 408 | (initLocalState [this] 409 | {:a 1}) 410 | (componentWillMount [this] 411 | (om/update-state! this update-in [:a] inc)) 412 | (render [this] 413 | (dom/div nil (str "a: " (om/get-state this :a))))) 414 | 415 | (defui CLPHN-3-Component-2 416 | Object 417 | (initLocalState [this] 418 | {:a 1}) 419 | (componentWillMount [this] 420 | (om/update-state! this update-in [:a] inc)) 421 | (render [this] 422 | (dom/div nil (str "a: " (om/get-state this :a))))) 423 | 424 | (defui CLPHN-3-Child 425 | Object 426 | (initLocalState [this] 427 | {:a 1}) 428 | (componentWillMount [this] 429 | (om/update-state! this update-in [:a] inc)) 430 | (render [this] 431 | (dom/div nil (str "child a: " (om/get-state this :a))))) 432 | 433 | (def clphn3-child (om/factory CLPHN-3-Child)) 434 | 435 | (defui CLPHN-3-Parent 436 | Object 437 | (initLocalState [this] 438 | {:a 2}) 439 | (componentWillMount [this] 440 | (om/update-state! this update-in [:a] inc)) 441 | (render [this] 442 | (dom/div nil 443 | (clphn3-child) 444 | (str "parent a: " (om/get-state this :a))))) 445 | 446 | (deftest test-clphn-3 447 | (let [c1 ((om/factory CLPHN-3-Component-1)) 448 | c2 ((om/factory CLPHN-3-Component-2)) 449 | c3 ((om/factory CLPHN-3-Parent))] 450 | (is (= (str (#'dom/render-to-str* c1)) 451 | "<div data-reactroot=\"\" data-reactid=\"1\">a: 2</div>")) 452 | (is (= (str (#'dom/render-to-str* c2)) 453 | "<div data-reactroot=\"\" data-reactid=\"1\">a: 2</div>")) 454 | (is (= (str (#'dom/render-to-str* c3)) 455 | (remove-whitespace "<div data-reactroot=\"\" data-reactid=\"1\"> 456 | <div data-reactid=\"2\">child a: 2</div> 457 | <!-- react-text: 3 -->parent a: 3<!-- /react-text --> 458 | </div>"))))) 459 | 460 | (defui SomeChild 461 | Object 462 | (render [this] 463 | (dom/div nil "foo"))) 464 | 465 | (def some-child (om/factory SomeChild)) 466 | 467 | (defui SomeParent 468 | Object 469 | (render [this] 470 | (some-child))) 471 | 472 | (deftest test-om-644 473 | (is (= (str (#'dom/render-to-str* ((om/factory SomeParent)))) 474 | "<div data-reactroot=\"\" data-reactid=\"1\">foo</div>"))) 475 | 476 | ;; React 15 477 | 478 | (defui React15Comp 479 | Object 480 | (render [this] 481 | (dom/div nil 482 | (dom/div nil 483 | "nested" 484 | (dom/div nil "other"))))) 485 | 486 | (deftest react-15-render 487 | (is (= (dom/render-to-str ((om/factory React15Comp))) 488 | (remove-whitespace "<div data-reactroot=\"\" data-reactid=\"1\" data-react-checksum=\"1635398171\"> 489 | <div data-reactid=\"2\"> 490 | <!-- react-text: 3 -->nested<!-- /react-text --> 491 | <div data-reactid=\"4\">other</div> 492 | </div> 493 | </div>"))) 494 | (is (= (dom/render-to-str 495 | (dom/div nil 496 | (dom/div nil 497 | (dom/div nil "3")) 498 | (dom/div nil 499 | (dom/div nil "5")))) 500 | (remove-whitespace "<div data-reactroot=\"\" data-reactid=\"1\" data-react-checksum=\"-1239993276\"> 501 | <div data-reactid=\"2\"> 502 | <div data-reactid=\"3\">3</div> 503 | </div> 504 | <div data-reactid=\"4\"> 505 | <div data-reactid=\"5\">5</div> 506 | </div> 507 | </div>")))) 508 | 509 | (deftest render-wrapped-attrs 510 | (is (= (str (#'dom/render-to-str* (dom/input #js {:value "foo" :id "bar" :type "text"}))) 511 | "<input type=\"text\" value=\"foo\" id=\"bar\" data-reactroot=\"\" data-reactid=\"1\"/>")) 512 | (is (= (str (#'dom/render-to-str* (dom/option #js {:disabled "" :label "foo" :selected ""}))) 513 | "<option selected=\"\" disabled=\"\" label=\"foo\" data-reactroot=\"\" data-reactid=\"1\"></option>")) 514 | ;; https://github.com/facebook/react/commit/fc0431 515 | (is (= (str (#'dom/render-to-str* (dom/input #js {:value "foo" :step 3 :name "points" :type "number"}))) 516 | "<input type=\"number\" step=\"3\" value=\"foo\" name=\"points\" data-reactroot=\"\" data-reactid=\"1\"/>")) 517 | (is (= (str (#'dom/render-to-str* (dom/input #js {:value "foo" :type "number" :name "points" :step 3}))) 518 | "<input type=\"number\" step=\"3\" value=\"foo\" name=\"points\" data-reactroot=\"\" data-reactid=\"1\"/>")) 519 | ;; https://github.com/facebook/react/commit/3013af 520 | (is (= (str (#'dom/render-to-str* (dom/input #js {:max 42 :value "foo" :min 10 :type "number" :name "points" :step 3}))) 521 | "<input type=\"number\" step=\"3\" min=\"10\" max=\"42\" value=\"foo\" name=\"points\" data-reactroot=\"\" data-reactid=\"1\"/>"))) 522 | 523 | (deftest test-om-800 524 | (let [sb (StringBuilder.)] 525 | (p/-render-to-string (dom/text-node "foo's") 0 sb) 526 | (is (= (str sb) "foo's"))) 527 | (let [sb (StringBuilder.)] 528 | (p/-render-to-string (dom/react-text-node "foo's") (volatile! 0) sb) 529 | (is (= (str sb) (str "<!-- react-text: 0 -->foo's<!-- /react-text -->"))))) 530 | 531 | (deftest test-om-818 532 | (let [sb (StringBuilder.)] 533 | (p/-render-to-string (dom/script #js {:type "text/javascript" 534 | :dangerouslySetInnerHTML {:__html "'foo bar'"}}) 535 | (volatile! 0) sb) 536 | (is (= (str sb) "<script type=\"text/javascript\" data-reactid=\"0\">'foo bar'</script>")))) 537 | 538 | (deftest test-om-799 539 | (let [elem (dom/create-element "use") 540 | sb (StringBuilder.)] 541 | (is (instance? om.dom.Element elem)) 542 | (p/-render-to-string elem (volatile! 0) sb) 543 | (is (= (str sb) "<use data-reactid=\"0\"></use>"))) 544 | (let [sb (StringBuilder.)] 545 | (p/-render-to-string (dom/create-element "use" nil "use-child") (volatile! 0) sb) 546 | (is (= (str sb) "<use data-reactid=\"0\">use-child</use>"))) 547 | (let [elem (dom/create-element "use" 548 | #js {:key "foo" 549 | :x "50" :y "10" 550 | :xlinkHref "#Port"} 551 | "use-child") 552 | sb (StringBuilder.)] 553 | (p/-render-to-string elem (volatile! 0) sb) 554 | (is (= (str sb) "<use x=\"50\" y=\"10\" xlink:href=\"#Port\" data-reactid=\"0\">use-child</use>")))) 555 | -------------------------------------------------------------------------------- /src/test/om/next/run_tests.cljs: -------------------------------------------------------------------------------- 1 | (ns om.next.run-tests 2 | (:require [cljs.test :refer-macros [run-tests]] 3 | [cljs.nodejs] 4 | [om.next.tests])) 5 | 6 | (enable-console-print!) 7 | 8 | (defn main [] 9 | (run-tests 'om.next.tests)) 10 | 11 | (set! *main-cli-fn* main) -------------------------------------------------------------------------------- /src/test/om/next/tutorials_test.clj: -------------------------------------------------------------------------------- 1 | (ns om.next.tutorials-test 2 | (:refer-clojure :exclude [read]) 3 | (:require [clojure.test :refer [deftest testing is are]] 4 | [om.test-utils :refer [remove-whitespace]] 5 | [om.next :as om :refer [defui]] 6 | [om.dom :as dom])) 7 | 8 | ;; ============================================================================= 9 | ;; Quick Start 10 | 11 | (def animals-app-state 12 | (atom 13 | {:app/title "Animals" 14 | :animals/list 15 | [[1 "Ant"] [2 "Antelope"] [3 "Bird"] [4 "Cat"] [5 "Dog"] 16 | [6 "Lion"] [7 "Mouse"] [8 "Monkey"] [9 "Snake"] [10 "Zebra"]]})) 17 | 18 | (defmulti animals-read (fn [env key params] key)) 19 | 20 | (defmethod animals-read :default 21 | [{:keys [state] :as env} key params] 22 | (let [st @state] 23 | (if-let [[_ value] (find st key)] 24 | {:value value} 25 | {:value :not-found}))) 26 | 27 | (defmethod animals-read :animals/list 28 | [{:keys [state] :as env} key {:keys [start end]}] 29 | {:value (subvec (:animals/list @state) start end)}) 30 | 31 | (defui AnimalsList 32 | static om/IQueryParams 33 | (params [this] 34 | {:start 0 :end 10}) 35 | static om/IQuery 36 | (query [this] 37 | '[:app/title (:animals/list {:start ?start :end ?end})]) 38 | Object 39 | (render [this] 40 | (let [{:keys [app/title animals/list]} (om/props this)] 41 | (dom/div nil 42 | (dom/h2 nil title) 43 | (apply dom/ul nil 44 | (map 45 | (fn [[i name]] 46 | (dom/li nil (str i ". " name))) 47 | list)))))) 48 | 49 | (def animals-reconciler 50 | (om/reconciler 51 | {:state animals-app-state 52 | :parser (om/parser {:read animals-read})})) 53 | 54 | (deftest test-render-animals-tutorial 55 | (let [result-markup (remove-whitespace 56 | "<div data-reactroot=\"\" data-reactid=\"1\"> 57 | <h2 data-reactid=\"2\">Animals</h2> 58 | <ul data-reactid=\"3\"> 59 | <li data-reactid=\"4\">1. Ant</li> 60 | <li data-reactid=\"5\">2. Antelope</li> 61 | <li data-reactid=\"6\">3. Bird</li> 62 | <li data-reactid=\"7\">4. Cat</li> 63 | <li data-reactid=\"8\">5. Dog</li> 64 | <li data-reactid=\"9\">6. Lion</li> 65 | <li data-reactid=\"10\">7. Mouse</li> 66 | <li data-reactid=\"11\">8. Monkey</li> 67 | <li data-reactid=\"12\">9. Snake</li> 68 | <li data-reactid=\"13\">10. Zebra</li> 69 | </ul> 70 | </div>")] 71 | (testing "render with factory" 72 | (let [ctor (om/factory AnimalsList)] 73 | (is (= (str (#'dom/render-to-str* (ctor @animals-app-state))) result-markup)))) 74 | (testing "render with reconciler & add-root!" 75 | (let [c (om/add-root! animals-reconciler AnimalsList nil) 76 | markup-str (str (#'dom/render-to-str* c))] 77 | (is (= (om/react-type (om/app-root animals-reconciler)) AnimalsList)) 78 | (is (= markup-str result-markup)))))) 79 | 80 | ;; ============================================================================= 81 | ;; Om Links tutorial 82 | 83 | (def links-init-data 84 | {:current-user {:email "bob.smith@gmail.com"} 85 | :items [{:id 0 :title "Foo"} 86 | {:id 1 :title "Bar"} 87 | {:id 2 :title "Baz"}]}) 88 | 89 | (defmulti links-read om/dispatch) 90 | 91 | (defmethod links-read :items 92 | [{:keys [query state]} k _] 93 | (let [st @state] 94 | {:value (om/db->tree query (get st k) st)})) 95 | 96 | (defui LinksItem 97 | static om/Ident 98 | (ident [_ {:keys [id]}] 99 | [:item/by-id id]) 100 | static om/IQuery 101 | (query [_] 102 | '[:id :title [:current-user _]]) 103 | Object 104 | (render [this] 105 | (let [{:keys [title current-user]} (om/props this)] 106 | (dom/li nil 107 | (dom/div nil title) 108 | (dom/div nil (:email current-user)))))) 109 | 110 | (def links-item (om/factory LinksItem)) 111 | 112 | (defui LinksSomeList 113 | static om/IQuery 114 | (query [_] 115 | [{:items (om/get-query LinksItem)}]) 116 | Object 117 | (render [this] 118 | (dom/div nil 119 | (dom/h2 nil "A List!") 120 | (dom/ul nil 121 | (map links-item (-> this om/props :items)))))) 122 | 123 | (def links-reconciler 124 | (om/reconciler 125 | {:state links-init-data 126 | :parser (om/parser {:read links-read})})) 127 | 128 | (deftest test-render-links-tutorial 129 | (let [c (om/add-root! links-reconciler LinksSomeList nil)] 130 | (is (= (str (#'dom/render-to-str* c)) 131 | (remove-whitespace 132 | "<div data-reactroot=\"\" data-reactid=\"1\"> 133 | <h2 data-reactid=\"2\">A List!</h2> 134 | <ul data-reactid=\"3\"> 135 | <li data-reactid=\"4\"> 136 | <div data-reactid=\"5\">Foo</div> 137 | <div data-reactid=\"6\">bob.smith@gmail.com</div> 138 | </li> 139 | <li data-reactid=\"7\"> 140 | <div data-reactid=\"8\">Bar</div> 141 | <div data-reactid=\"9\">bob.smith@gmail.com</div> 142 | </li> 143 | <li data-reactid=\"10\"> 144 | <div data-reactid=\"11\">Baz</div> 145 | <div data-reactid=\"12\">bob.smith@gmail.com</div> 146 | </li> 147 | </ul> 148 | </div>"))))) 149 | 150 | ;; ============================================================================= 151 | ;; Componentes, Identity & Normalization 152 | 153 | (def cian-init-data 154 | {:list/one [{:name "John" :points 0} 155 | {:name "Mary" :points 0} 156 | {:name "Bob" :points 0}] 157 | :list/two [{:name "Mary" :points 0 :age 27} 158 | {:name "Gwen" :points 0} 159 | {:name "Jeff" :points 0}]}) 160 | 161 | ;; ----------------------------------------------------------------------------- 162 | ;; Parsing 163 | 164 | (defmulti cian-read om/dispatch) 165 | 166 | (defn get-people [state key] 167 | (let [st @state] 168 | (into [] (map #(get-in st %)) (get st key)))) 169 | 170 | (defmethod cian-read :list/one 171 | [{:keys [state] :as env} key params] 172 | {:value (get-people state key)}) 173 | 174 | (defmethod cian-read :list/two 175 | [{:keys [state] :as env} key params] 176 | {:value (get-people state key)}) 177 | 178 | (defmulti cian-mutate om/dispatch) 179 | 180 | (defmethod cian-mutate 'points/increment 181 | [{:keys [state]} _ {:keys [name]}] 182 | {:action 183 | (fn [] 184 | (swap! state update-in 185 | [:person/by-name name :points] 186 | inc))}) 187 | 188 | (defmethod cian-mutate 'points/decrement 189 | [{:keys [state]} _ {:keys [name]}] 190 | {:action 191 | (fn [] 192 | (swap! state update-in 193 | [:person/by-name name :points] 194 | #(let [n (dec %)] (if (neg? n) 0 n))))}) 195 | 196 | ;; ----------------------------------------------------------------------------- 197 | ;; Components 198 | 199 | (defui Person 200 | static om/Ident 201 | (ident [this {:keys [name]}] 202 | [:person/by-name name]) 203 | static om/IQuery 204 | (query [this] 205 | '[:name :points :age]) 206 | Object 207 | (render [this] 208 | (println "Render Person" (-> this om/props :name)) 209 | (let [{:keys [points name foo] :as props} (om/props this)] 210 | (dom/li nil 211 | (dom/label nil (str name ", points: " points)) 212 | (dom/button 213 | #js {:onClick 214 | (fn [e] 215 | (om/transact! this 216 | `[(points/increment ~props)]))} 217 | "+") 218 | (dom/button 219 | #js {:onClick 220 | (fn [e] 221 | (om/transact! this 222 | `[(points/decrement ~props)]))} 223 | "-"))))) 224 | 225 | (def person (om/factory Person {:keyfn :name})) 226 | 227 | (defui ListView 228 | Object 229 | (render [this] 230 | ;(println "Render ListView" (-> this om/path first)) 231 | (let [list (om/props this)] 232 | (apply dom/ul nil 233 | (map person list))))) 234 | 235 | (def list-view (om/factory ListView)) 236 | 237 | (defui RootView 238 | static om/IQuery 239 | (query [this] 240 | (let [subquery (om/get-query Person)] 241 | `[{:list/one ~subquery} {:list/two ~subquery}])) 242 | Object 243 | (render [this] 244 | (println "Render RootView") 245 | (let [{:keys [list/one list/two]} (om/props this)] 246 | (apply dom/div nil 247 | [(dom/h2 nil "List A") 248 | (list-view one) 249 | (dom/h2 nil "List B") 250 | (list-view two)])))) 251 | 252 | (def cian-reconciler 253 | (om/reconciler 254 | {:state cian-init-data 255 | :parser (om/parser {:read cian-read :mutate cian-mutate})})) 256 | 257 | (deftest test-cian-tutorial 258 | (let [c (om/add-root! cian-reconciler RootView nil)] 259 | (is (= (str (#'dom/render-to-str* c)) 260 | (remove-whitespace "<div data-reactroot=\"\" data-reactid=\"1\"> 261 | <h2 data-reactid=\"2\">List A</h2> 262 | <ul data-reactid=\"3\"> 263 | <li data-reactid=\"4\"> 264 | <label data-reactid=\"5\">John, points: 0</label> 265 | <button data-reactid=\"6\">+</button> 266 | <button data-reactid=\"7\">-</button> 267 | </li> 268 | <li data-reactid=\"8\"> 269 | <label data-reactid=\"9\">Mary, points: 0</label> 270 | <button data-reactid=\"10\">+</button> 271 | <button data-reactid=\"11\">-</button> 272 | </li> 273 | <li data-reactid=\"12\"> 274 | <label data-reactid=\"13\">Bob, points: 0</label> 275 | <button data-reactid=\"14\">+</button> 276 | <button data-reactid=\"15\">-</button> 277 | </li> 278 | </ul> 279 | <h2 data-reactid=\"16\">List B</h2> 280 | <ul data-reactid=\"17\"> 281 | <li data-reactid=\"18\"> 282 | <label data-reactid=\"19\">Mary, points: 0</label> 283 | <button data-reactid=\"20\">+</button> 284 | <button data-reactid=\"21\">-</button> 285 | </li> 286 | <li data-reactid=\"22\"> 287 | <label data-reactid=\"23\">Gwen, points: 0</label> 288 | <button data-reactid=\"24\">+</button> 289 | <button data-reactid=\"25\">-</button> 290 | </li> 291 | <li data-reactid=\"26\"> 292 | <label data-reactid=\"27\">Jeff, points: 0</label> 293 | <button data-reactid=\"28\">+</button> 294 | <button data-reactid=\"29\">-</button> 295 | </li> 296 | </ul> 297 | </div>"))))) 298 | 299 | ;; ============================================================================= 300 | ;; Queries with unions 301 | 302 | (def union-init-data 303 | {:dashboard/items 304 | [{:id 0 :type :dashboard/post 305 | :author "Laura Smith" 306 | :title "A Post!" 307 | :content "Lorem ipsum dolor sit amet, quem atomorum te quo" 308 | :favorites 0} 309 | {:id 1 :type :dashboard/photo 310 | :title "A Photo!" 311 | :image "photo.jpg" 312 | :caption "Lorem ipsum" 313 | :favorites 0} 314 | {:id 2 :type :dashboard/post 315 | :author "Jim Jacobs" 316 | :title "Another Post!" 317 | :content "Lorem ipsum dolor sit amet, quem atomorum te quo" 318 | :favorites 0} 319 | {:id 3 :type :dashboard/graphic 320 | :title "Charts and Stufff!" 321 | :image "chart.jpg" 322 | :favorites 0} 323 | {:id 4 :type :dashboard/post 324 | :author "May Fields" 325 | :title "Yet Another Post!" 326 | :content "Lorem ipsum dolor sit amet, quem atomorum te quo" 327 | :favorites 0}]}) 328 | 329 | (defui Post 330 | static om/IQuery 331 | (query [this] 332 | [:id :type :title :author :content]) 333 | Object 334 | (render [this] 335 | (let [{:keys [title author content] :as props} (om/props this)] 336 | (dom/div nil 337 | (dom/h3 nil title) 338 | (dom/h4 nil author) 339 | (dom/p nil content))))) 340 | 341 | (def post (om/factory Post)) 342 | 343 | (defui Photo 344 | static om/IQuery 345 | (query [this] 346 | [:id :type :title :image :caption]) 347 | Object 348 | (render [this] 349 | (let [{:keys [title image caption]} (om/props this)] 350 | (dom/div nil 351 | (dom/h3 nil (str "Photo: " title)) 352 | (dom/div nil image) 353 | (dom/p nil "Caption: "))))) 354 | 355 | (def photo (om/factory Photo)) 356 | 357 | (defui Graphic 358 | static om/IQuery 359 | (query [this] 360 | [:id :type :title :image]) 361 | Object 362 | (render [this] 363 | (let [{:keys [title image]} (om/props this)] 364 | (dom/div nil 365 | (dom/h3 nil (str "Graphic: " title)) 366 | (dom/div nil image))))) 367 | 368 | (def graphic (om/factory Graphic)) 369 | 370 | (defui DashboardItem 371 | static om/Ident 372 | (ident [this {:keys [id type]}] 373 | [type id]) 374 | static om/IQuery 375 | (query [this] 376 | (zipmap 377 | [:dashboard/post :dashboard/photo :dashboard/graphic] 378 | (map #(conj % :favorites) 379 | [(om/get-query Post) 380 | (om/get-query Photo) 381 | (om/get-query Graphic)]))) 382 | Object 383 | (render [this] 384 | (let [{:keys [id type favorites] :as props} (om/props this)] 385 | (dom/li 386 | #js {:style #js {:padding 10 :borderBottom "1px solid black"}} 387 | (dom/div nil 388 | (({:dashboard/post post 389 | :dashboard/photo photo 390 | :dashboard/graphic graphic} type) 391 | (om/props this))) 392 | (dom/div nil 393 | (dom/p nil (str "Favorites: " favorites)) 394 | (dom/button 395 | #js {:onClick 396 | (fn [e] 397 | (om/transact! this 398 | `[(dashboard/favorite {:ref [~type ~id]})]))} 399 | "Favorite!")))))) 400 | 401 | (def dashboard-item (om/factory DashboardItem)) 402 | 403 | (defui Dashboard 404 | static om/IQuery 405 | (query [this] 406 | [{:dashboard/items (om/get-query DashboardItem)}]) 407 | Object 408 | (render [this] 409 | (let [{:keys [dashboard/items]} (om/props this)] 410 | (apply dom/ul 411 | #js {:style #js {:padding 0}} 412 | (map dashboard-item items))))) 413 | 414 | (defmulti union-read om/dispatch) 415 | 416 | (defmethod union-read :dashboard/items 417 | [{:keys [state]} k _] 418 | (let [st @state] 419 | {:value (into [] (map #(get-in st %)) (get st k))})) 420 | 421 | (defmulti mutate om/dispatch) 422 | 423 | (defmethod mutate 'dashboard/favorite 424 | [{:keys [state]} k {:keys [ref]}] 425 | {:action 426 | (fn [] 427 | (swap! state update-in (conj ref :favorites) inc))}) 428 | 429 | (def union-reconciler 430 | (om/reconciler 431 | {:state union-init-data 432 | :parser (om/parser {:read union-read :mutate mutate})})) 433 | 434 | 435 | (deftest test-unions-tutorial 436 | (let [c (om/add-root! union-reconciler Dashboard nil)] 437 | (is (= (str (#'dom/render-to-str* c)) 438 | (remove-whitespace 439 | "<ul style=\"padding:0;\" data-reactroot=\"\" data-reactid=\"1\"> 440 | <li style=\"padding:10px;border-bottom:1px solid black;\" data-reactid=\"2\"> 441 | <div data-reactid=\"3\"> 442 | <div data-reactid=\"4\"> 443 | <h3 data-reactid=\"5\">A Post!</h3> 444 | <h4 data-reactid=\"6\">Laura Smith</h4> 445 | <p data-reactid=\"7\">Lorem ipsum dolor sit amet, quem atomorum te quo</p> 446 | </div> 447 | </div> 448 | <div data-reactid=\"8\"> 449 | <p data-reactid=\"9\">Favorites: 0</p> 450 | <button data-reactid=\"10\">Favorite!</button> 451 | </div> 452 | </li> 453 | <li style=\"padding:10px;border-bottom:1px solid black;\" data-reactid=\"11\"> 454 | <div data-reactid=\"12\"> 455 | <div data-reactid=\"13\"> 456 | <h3 data-reactid=\"14\">Photo: A Photo!</h3> 457 | <div data-reactid=\"15\">photo.jpg</div> 458 | <p data-reactid=\"16\">Caption: </p> 459 | </div> 460 | </div> 461 | <div data-reactid=\"17\"> 462 | <p data-reactid=\"18\">Favorites: 0</p> 463 | <button data-reactid=\"19\">Favorite!</button> 464 | </div> 465 | </li> 466 | <li style=\"padding:10px;border-bottom:1px solid black;\" data-reactid=\"20\"> 467 | <div data-reactid=\"21\"> 468 | <div data-reactid=\"22\"> 469 | <h3 data-reactid=\"23\">Another Post!</h3> 470 | <h4 data-reactid=\"24\">Jim Jacobs</h4> 471 | <p data-reactid=\"25\">Lorem ipsum dolor sit amet, quem atomorum te quo</p> 472 | </div> 473 | </div> 474 | <div data-reactid=\"26\"> 475 | <p data-reactid=\"27\">Favorites: 0</p> 476 | <button data-reactid=\"28\">Favorite!</button> 477 | </div> 478 | </li> 479 | <li style=\"padding:10px;border-bottom:1px solid black;\" data-reactid=\"29\"> 480 | <div data-reactid=\"30\"> 481 | <div data-reactid=\"31\"> 482 | <h3 data-reactid=\"32\">Graphic: Charts and Stufff!</h3> 483 | <div data-reactid=\"33\">chart.jpg</div> 484 | </div> 485 | </div> 486 | <div data-reactid=\"34\"> 487 | <p data-reactid=\"35\">Favorites: 0</p> 488 | <button data-reactid=\"36\">Favorite!</button> 489 | </div> 490 | </li> 491 | <li style=\"padding:10px;border-bottom:1px solid black;\" data-reactid=\"37\"> 492 | <div data-reactid=\"38\"> 493 | <div data-reactid=\"39\"> 494 | <h3 data-reactid=\"40\">Yet Another Post!</h3> 495 | <h4 data-reactid=\"41\">May Fields</h4> 496 | <p data-reactid=\"42\">Lorem ipsum dolor sit amet, quem atomorum te quo</p> 497 | </div> 498 | </div> 499 | <div data-reactid=\"43\"> 500 | <p data-reactid=\"44\">Favorites: 0</p> 501 | <button data-reactid=\"45\">Favorite!</button> 502 | </div> 503 | </li> 504 | </ul>"))))) 505 | -------------------------------------------------------------------------------- /src/test/om/test_utils.clj: -------------------------------------------------------------------------------- 1 | (ns om.test-utils 2 | (:require [clojure.string :as str])) 3 | 4 | (defn remove-whitespace [s] 5 | (str/replace s #"(>)\s+(<)" "$1$2")) 6 | -------------------------------------------------------------------------------- /src/test/om/tests.cljs: -------------------------------------------------------------------------------- 1 | (ns om.tests 2 | (:require [cljs.test :refer-macros [is are deftest run-tests]] 3 | [om.core :as om :include-macros true] 4 | [om.dom :as dom :include-macros true])) 5 | 6 | (enable-console-print!) 7 | 8 | (defprotocol IFoo 9 | (-foo [this])) 10 | 11 | (defn derive* [cursor] 12 | (specify cursor 13 | om/ICursorDerive 14 | (-derive [this derived state path] 15 | (derive* (om/to-cursor derived state path))) 16 | IFoo 17 | (-foo [_] :foo))) 18 | 19 | (deftest cursor-protocols 20 | (is (om/cursor? (om/to-cursor [1 2 3]))) 21 | (is (om/cursor? (om/to-cursor {:foo "bar"}))) 22 | (are [x y] (= x y) 23 | (.-value (om/to-cursor [1 2 3])) [1 2 3] 24 | (.-value (om/to-cursor {:foo "bar"})) {:foo "bar"} 25 | (first (om/to-cursor [1 2 3])) 1 26 | (first (om/to-cursor {:foo "bar"})) [:foo "bar"] 27 | (:foo (first (om/to-cursor [{:foo "bar"}]))) "bar" 28 | (.-path (first (om/to-cursor [{:foo "bar"}]))) [0] 29 | (.-path (first (rest (om/to-cursor [{:foo "bar"} {:baz "woz"}])))) [1] 30 | (rest (rest (om/to-cursor [{:foo "bar"} {:baz "woz"}]))) () 31 | (.-path (first (next (om/to-cursor [{:foo "bar"} {:baz "woz"}])))) [1] 32 | (.-path (get-in (om/to-cursor {:foo [{:id 1}]}) [:foo 0])) [:foo 0] 33 | (get-in (om/to-cursor {:foo [{:id 1}]}) [:foo 0 :id]) 1 34 | (assoc (om/to-cursor {:foo 1}) :bar 2) {:foo 1 :bar 2} 35 | {:foo 1 :bar 2} (assoc (om/to-cursor {:foo 1}) :bar 2) 36 | (dissoc (om/to-cursor {:foo 1}) :foo) {} 37 | {} (dissoc (om/to-cursor {:foo 1}) :foo) 38 | (map identity (om/to-cursor [{:id 1} {:id 2} {:id 3}])) 39 | [{:id 1} {:id 2} {:id 3}] 40 | [{:id 1} {:id 2} {:id 3}] 41 | (map identity (om/to-cursor [{:id 1} {:id 2} {:id 3}]))) 42 | (let [c (derive* (om/to-cursor {:foo {:bar {:baz 'woz}}} [] nil))] 43 | (is (= (-foo (get-in c [:foo :bar])) :foo)))) 44 | 45 | (run-tests) 46 | --------------------------------------------------------------------------------