├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── deps.edn ├── doc ├── cljdoc.edn ├── optimizations.md ├── react-hooks.md ├── react-interop.md ├── react-suspense-and-code-splitting.md └── useful-mixins.md ├── examples └── rum │ ├── examples.cljs │ ├── examples │ ├── binary_clock.cljc │ ├── bmi_calculator.cljc │ ├── board_reactive.cljc │ ├── context.cljs │ ├── controls.cljc │ ├── core.cljc │ ├── custom_props.cljs │ ├── errors.cljc │ ├── form_validation.cljs │ ├── inputs.cljc │ ├── js_components.cljc │ ├── keys.cljc │ ├── local_state.cljc │ ├── multiple_return.cljc │ ├── portals.cljc │ ├── refs.cljc │ ├── self_reference.cljc │ ├── timer_reactive.cljc │ └── timer_static.cljc │ └── examples_page.clj ├── index.html ├── perf ├── pages │ ├── page1.html │ ├── page2.html │ └── page3.html └── rum │ └── perf.clj ├── project.clj ├── scripts └── test ├── src ├── daiquiri │ ├── compiler.clj │ ├── core.clj │ ├── core.cljs │ ├── interpreter.cljs │ ├── normalize.cljc │ └── util.cljc └── rum │ ├── core.clj │ ├── core.cljs │ ├── cursor.clj │ ├── cursor.cljs │ ├── derived_atom.cljc │ ├── lazy_loader.cljc │ ├── server_render.clj │ ├── specs.cljc │ └── util.cljc ├── target └── main.js └── test ├── daiquiri ├── compiler_test.clj ├── interpreter_test.cljs ├── normalize_test.cljc └── util_test.cljc ├── rum └── test │ ├── cursor.clj │ ├── defc.clj │ ├── derived_atom.clj │ ├── react_render_html.js │ ├── server.clj │ └── server_render.cljc └── test_runner.cljs /.gitignore: -------------------------------------------------------------------------------- 1 | /target/** 2 | !/target/main.js 3 | /classes 4 | /checkouts 5 | pom.xml 6 | pom.xml.asc 7 | *.jar 8 | *.class 9 | /.lein-* 10 | /.nrepl-port 11 | .DS_Store 12 | .nrepl-history 13 | .idea 14 | rum.iml 15 | .cljs_node_repl/ 16 | .cpcache/ 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.12.11 2 | 3 | - Support `multiple` flag for `select` #259 thx @tomasd 4 | 5 | ## 0.12.10 6 | 7 | - add ^js typehint to React Context #251 #256 thx @alexdao3 8 | 9 | ## 0.12.9 10 | 11 | - restored :will-update for backward compatibility #249 thx @tiensonqin 12 | 13 | ## 0.12.8 14 | 15 | ### Fixes 16 | 17 | - Bringing back async rendering queue to address performance issues in existing clients 18 | 19 | ## 0.12.7 20 | 21 | ### Fixes 22 | 23 | - Actually fixed broken interpretation of a collection of elements, due to incorrect `fragment-tag?` check #235 24 | 25 | ## 0.12.6 26 | 27 | ### Fixes 28 | 29 | - Fixed broken interpretation of a collection of elements, due to incorrect `fragment?` check #235 30 | 31 | ## 0.12.5 32 | 33 | ### Fixes 34 | 35 | - Fixed warnings not being disabled in shadow-cljs @Azzurite 36 | 37 | ## 0.12.4 38 | 39 | ### New 40 | 41 | - `:did-remount` is now `:will-remount`, the name matches semantics closely 42 | - Added support for soft-deprecated lifecycle methods prefixed with `UNSAFE`: `:unsafe/will-mount` and `:unsafe/will-update` 43 | - Added a wrapper for React's `useLayoutEffect` hook: `use-layout-effect!` 44 | - Added Reagent-like `:>` syntax for interop with React components 45 | 46 | ### Fixes 47 | 48 | - Fixed `fragment` macro not supporting optional attributes 49 | - Fixed crashing in projects using Rum w/o ClojureScript dependency 50 | 51 | ## 0.12.3 52 | 53 | ### Fixes 54 | 55 | - Fixed `rum/local` & `rum/reactive` components not being updated when `rum/static` is enabled [#221](https://github.com/tonsky/rum/issues/221) 56 | 57 | ## 0.12.2 58 | 59 | ### Fixes 60 | 61 | - Added missing `:did-update` and `:key-fn` to specs 62 | 63 | ## 0.12.1 64 | 65 | ### New 66 | 67 | - Changed the order of arguments in `use-reducer`'s `reducer-fn` to match React [#213](https://github.com/tonsky/rum/issues/213) 68 | - String attribute keys won't be camel cased in ClojureScript, allowing custom attributes [#129](https://github.com/tonsky/rum/issues/129) 69 | - Added assertions to check incorrect mixin keys [#96](https://github.com/tonsky/rum/issues/96) 70 | - Added `rum.core/set-warn-on-interpretation!` that enables warnings when compiler emits intepretation calls [66d352](https://github.com/tonsky/rum/commit/66d352acdedb5acc5bb860a7fc30411eac67c30c) 71 | - Added README section about Hiccup pre-compilation and interpretation [#103](https://github.com/tonsky/rum/pull/103) 72 | 73 | ### Fixes 74 | 75 | - Fixed `defcontext` macro [#214](https://github.com/tonsky/rum/issues/214) 76 | 77 | ## 0.12.0 78 | 79 | ### Dependencies 80 | 81 | - Upgraded to ClojureScript 1.10.773 82 | 83 | ### Breaking 84 | 85 | - Removed custom update scheduling mechanism (hopefully doesn't break anything) 86 | - Replaced Sablono with Daiquiri, reworked Sablono fork (in case if you are depending on Sablono) 87 | 88 | ### New 89 | 90 | - Ported Sablono's test suite 91 | - Added unit tests runner on Node 92 | - Added alternative Hiccup syntax for React.Fragment `:<>` 93 | 94 | ## 0.11.5 95 | 96 | ### Dependencies 97 | 98 | - Rum now requires Clojure 1.9.0 99 | - Upgraded to ClojureScript 1.10.597 100 | - Upgraded to React 16.8.6 101 | 102 | ### Deprecations 103 | 104 | - Deprecated usage of string refs 105 | - Deprecated legacy React Context API of string refs 106 | 107 | ### New 108 | 109 | - Added `deps.edn` 110 | - Added `use-state`, `use-reducer`, `use-effect!`, `use-callback`, `use-memo` and `use-ref` hooks 111 | - Added `rum.lazy-loader` ns and `suspense` component 112 | - Added `fragment` component 113 | - Added JS SSR API ([#105](https://github.com/tonsky/rum/issues/105)) 114 | - Added React Context API 115 | - Changed component's `displayName` to a fully qualified var name e.g. `app.core/button` 116 | - Added `React.createRef` API 117 | - Added adapter for JavaScript React components to be used in Rum with a fallback hook to render on JVM 118 | 119 | ### Fixes 120 | 121 | - Fixed `:type` attribute value serialization on JVM SSR ([#120](https://github.com/tonsky/rum/issues/120)) 122 | - Fixed an error when calling `rum/with-key` on multiple return components on JVM SSR ([#185](https://github.com/tonsky/rum/issues/185)) 123 | - Fixed string escaping for `:class` and `:type` attribute values ([#93](https://github.com/tonsky/rum/issues/93)) 124 | - Fixed unsused components not removed in production builds 125 | 126 | ## 0.11.4 127 | 128 | - Fix render-all to forbid forceUpdate on falsy comp ([#193](https://github.com/tonsky/rum/pull/193), thx @FieryCod) 129 | 130 | ## 0.11.3 131 | 132 | - Docstrings for https://cljdoc.org/d/rum/rum 133 | 134 | ## 0.11.2 135 | 136 | - Server-render on-\* event handlers with string values 137 | 138 | ## 0.11.1 139 | 140 | - Sablono or CLJS are excluded completely when using SSR ([#83](https://github.com/tonsky/rum/issues/83), [#157](https://github.com/tonsky/rum/pull/157)) 141 | 142 | ## 0.11.0 (thx [Roman Liutikov](https://github.com/roman01la) & [Alexander Solovyov](https://github.com/piranha), [#151](https://github.com/tonsky/rum/pull/151)) 143 | 144 | - [ BREAKING ] `contextTypes` and `childContextTypes` should be specified through `:static-properties` instead of `:class-properties` 145 | - React 16.2.0, Sablono 0.8.1 146 | - Added `rum/portal` method 147 | - Added `:did-catch` lifecycle callback 148 | - Added `rum/hydrate` and updated SSR output to match React’s 149 | 150 | ## 0.10.8 151 | 152 | - React 15.4.2-0, Sablono 0.7.7 153 | - Render boolean `aria-*` values as strings (thx [r0man](https://github.com/r0man), [#114](https://github.com/tonsky/rum/pull/114)) 154 | - Escape attributes during server-side rendering (thx [Alexander Solovyov](https://github.com/piranha), [#115](https://github.com/tonsky/rum/pull/115)) 155 | 156 | ## 0.10.7 157 | 158 | - Fixed server-side rendering discrepancy ([#99](https://github.com/tonsky/rum/issues/99)) 159 | - Sablono 0.7.5, React 15.3.1-0 160 | 161 | ## 0.10.6 162 | 163 | - Sablono 0.7.4 [fixes the issue](https://github.com/r0man/sablono/pull/129) with controlling components refusing to change value if non-string value was used 164 | - React 15.3.0-0 165 | - Throw error when `<` is misplaced in `defc` (thx [Martin Klepsch](https://github.com/martinklepsch), [#88](https://github.com/tonsky/rum/issues/88), [#90](https://github.com/tonsky/rum/pull/90)) 166 | 167 | ## 0.10.5 168 | 169 | - Sablono 0.7.3 fixes the issue when IE lost keystrokes in controlled inputs/textarea ([#86](https://github.com/tonsky/rum/issues/86)) 170 | - React 15.2.1-1 171 | - Warn when `rum.core/react` is used without `rum.core/reactive` (thx [Martin Klepsch](https://github.com/martinklepsch), [#82](https://github.com/tonsky/rum/issues/82), [#87](https://github.com/tonsky/rum/pull/87)) 172 | 173 | ## 0.10.4 174 | 175 | - Ability to use `:pre` and `:post` checks in `rum.core/defc` (thx [Martin Klepsch](https://github.com/martinklepsch), [#81](https://github.com/tonsky/rum/pull/81)) 176 | 177 | ## 0.10.3 178 | 179 | - Fixed regression of `displayName` in 0.10.0 180 | - Bumped React to 15.2.0 181 | 182 | ## 0.10.2 183 | 184 | - Fixed a bug when `:before-render` and `:will-update` weren’t called on subsequent renders 185 | 186 | ## 0.10.1 187 | 188 | - Made `rum.core/state` public again 189 | - `:before-render` should be called on server-side rendering too (thx [Alexander Solovyov](https://github.com/piranha), [#79](https://github.com/tonsky/rum/pull/79)) 190 | 191 | ## 0.10.0 192 | 193 | A big cleanup/optmization/goodies release with a lot breaking changes. Read carefully! 194 | 195 | - [ BREAKING ] `cursor` got renamed to `cursor-in`. New `cursor` method added that takes single key (as everywhere in Clojure) 196 | - [ BREAKING ] `rum/mount` returns `nil` (because you [shouldn’t rely on return value of ReactDOM.render](https://github.com/facebook/react/issues/4936)) 197 | - [ BREAKING ] `:transfer-state` is gone. All of component’s state is now transferred by default. If you still need to do something fancy on `componentWillReceiveProps`, new callback is called `:did-remount` callback 198 | - [ BREAKING ] removed `cursored` and `cursored-watch` mixins. They felt too unnatural to use 199 | - [ BREAKING ] removed `rum/with-props` (deprecated since 0.3.0). Use `rum/with-key` and `rum/with-ref` instead 200 | - [ BREAKING ] server-side rendering no longer calls `:did-mount` (obviously, that was a mistake) 201 | - [ BREAKING ] `:rum/id` is gone. If you need an unique id per component, allocate one in `:init` as store it in state under namespaced key 202 | 203 | When upgrading to 0.10.0, check this migration checklist: 204 | 205 | - Change all `rum/cursor` calls to `rum/cursor-in` 206 | - Find all `:transfer-state` mixins. 207 | - If the only thing they were doing is something like `(fn [old new] (assoc new ::key (::key old)))`, just delete them. 208 | - If not, rename to `:did-remount` 209 | - Check if you were using `rum/mount` return value. If yes, find another way to obtain component (e.g. via `ref`, `defcc` etc) 210 | - Replace `rum/with-props` with `rum/with-key`, `rum/with-ref` or `:key-fn` 211 | - Check that you weren’t relying on `:did-mount` in server-side rendering 212 | 213 | Now for the good stuff: 214 | 215 | - Cursors now support metadata, `alter-meta!` etc 216 | - Cursors can be used from Clojure 217 | - Added `:key-fn` to mixins. That function will be called before element creation, with same arguments as render fn, and its return value will be used as a key on that element 218 | - Mixins can specify `:before-render` (triggered at `componentWillMount` and `componentWillUpdate`) and `:after-render` (`componentDidMount` and `componentDidUpdate`) callback 219 | - Added `rum/ref` and `rum/ref-node` helpers, returning backing component and DOM node 220 | - Some client-side API functions added to server version (`dom-node`, `unmount`, `request-render` etc). Their implementation just throws an exception. This is to help you write less conditional directives in e.g. `:did-mount` or `:will-unmount` mixins. They will never be called, but won’t stop code from compiling either. 221 | 222 | And couple of optimizations: 223 | 224 | - Rum now makes use of staless components (nothing for you to do, if your component is defined via `defc` with no mixins, it’ll be automatically compiled to stateless component) 225 | - Rum will use React’s batched updates to perform rendering on `requestAnimationFrame` in a single chunk 226 | - Streamlined internals of component construction, removed `render->mixin`, `args->state`, `element` and `ctor->class` 227 | 228 | ## 0.9.1 229 | 230 | - Added `rum.core/derived-atom`, a function that let you build reactive chains and directed acyclic graphs of dependent atoms. E.g. you want `*c` to always contain a value of `*a` plus a value of `*b` and update whenever any of them changes 231 | - Added `rum.core/dom-node` helper that takes state and finds corresponding top DOM node of a component. Can be called in mixins after initial render only 232 | - Fixed compatibility of `with-key` on nil-returning component in server rendering (thx [Alexander Solovyov](https://github.com/piranha), [#73](https://github.com/tonsky/rum/pull/73)) 233 | 234 | ## 0.9.0 235 | 236 | - Better support for server-side rendering of SVG 237 | - [ BREAKING ] Rum used to support multiple ways to specify attributes. You would expect that both `:allow-full-screen`, `:allowFullScreen` and `"allowFullScreen"` would be normalized to `allowfullscreen`. As a result, you have to face three problems: 238 | - how do I decide which variant to use? 239 | - how do I ensure consistency accross my team and our codebase? 240 | - find & replace become harder 241 | 242 | Starting with 0.9.0, Rum will adopt “There’s Only One Way To Do It” policy. All attributes MUST be specified as kebab-cased keywords: 243 | 244 | | Attribute | What to use | What not to use | 245 | | -------------------- | ----------------------------------------------- | ----------------------------------------------------- | 246 | | class | `:class` | ~~`:class-name`~~ ~~`:className`~~ | 247 | | for | `:for` | ~~`:html-for`~~ ~~`:htmlFor`~~ | 248 | | unescaped innerHTML | `:dangerouslySetInnerHTML { :__html { "..." }}` | | 249 | | uncontrolled value | `:default-value` | ~~`:defaultValue`~~ | 250 | | uncontrolled checked | `:default-checked` | ~~`:defaultChecked`~~ | 251 | | itemid, classid | `:item-id`, `:class-id` | ~~`:itemID`~~ ~~`:itemId`~~ ~~`:itemid`~~ | 252 | | xml:lang etc | `:xml-lang` | ~~`:xml/lang`~~ ~~`:xmlLang`~~ ~~`"xml:lang"`~~ | 253 | | xlink:href etc | `:xlink-href` | ~~`:xlink/href`~~ ~~`:xlinkHref`~~ ~~`"xlink:href"`~~ | 254 | | xmlns | not supported | | 255 | 256 | To migrate to 0.9.0 from earlier versions, just do search-and-replace for non-standard variants and replace them with recommended ones. 257 | 258 | ## 0.8.4 259 | 260 | - Improved server-side rendering for inputs ([#67](https://github.com/tonsky/rum/issues/67) & beyond) 261 | - Compatible server-side rendering of components that return nil ([#64](https://github.com/tonsky/rum/issues/64)) 262 | - Upgraded React to 15.1.0 263 | 264 | ## 0.8.3 265 | 266 | - `rum/render-static-markup` call for pure HTML templating. Use it if you’re not planning to connect your page with React later 267 | - `rum/def*` macros now correctly retain metadata that already exists on a symbol (thx [aJchemist](https://github.com/aJchemist), [#62](https://github.com/tonsky/rum/pull/62)) 268 | 269 | ## 0.8.2 270 | 271 | - Add `rum.core/unmount` function (thx [emnh](https://github.com/emnh), [#61](https://github.com/tonsky/rum/issues/61)) 272 | 273 | ## 0.8.1 274 | 275 | - Retain `:arglists` metadata on vars defined by `rum/def*` macros (thx [aJchemist](https://github.com/aJchemist), [#60](https://github.com/tonsky/rum/pull/60)) 276 | 277 | ## 0.8.0 278 | 279 | - Migrated to React 15.0.1 280 | - Optimized server-side rendering (~4× faster than Rum 0.7.0, ~2-3× faster than Hiccup 1.0.5) 281 | 282 | ## 0.7.0 283 | 284 | - Server-side rendering via `rum/render-html` (thx [Alexander Solovyov](https://github.com/piranha)) 285 | 286 | ## 0.6.0 287 | 288 | - [ BREAKING ] Updated to [React 0.14.3](https://facebook.github.io/react/blog/2015/10/07/react-v0.14.html) (thx [Andrey Antukh](https://github.com/niwinz), [#53](https://github.com/tonsky/rum/pull/53)) 289 | 290 | ## 0.5.0 291 | 292 | - Added `:class-properties` to define arbitrary properties on a React class (thx [Karanbir Toor](https://github.com/currentoor), [#44](https://github.com/tonsky/rum/pull/44)) 293 | - [ BREAKING ] Removed support for `:child-context-types` and `:context-types`. Use `{ :class-properties { :childContextTypes ..., :contextTypes ... } }` instead. 294 | 295 | ## 0.4.2 296 | 297 | - Check for `setTimeout` in global scope instead of in window (thx [Alexander Solovyov](https://github.com/piranha), [#43](https://github.com/tonsky/rum/pull/43)) 298 | 299 | ## 0.4.1 300 | 301 | - Fixed bug with rum macros emitting wrong namespace. You can now require `rum.core` under any alias you want (thx [Stuart Hinson](https://github.com/stuarth), [#42](https://github.com/tonsky/rum/pull/42)) 302 | 303 | ## 0.4.0 304 | 305 | - [ BREAKING ] Core namespace was renamed from `rum` to `rum.core` to supress CLJS warnings 306 | 307 | ## 0.3.0 308 | 309 | - Upgraded to React 0.13.3, Sablono 0.3.6, ClojueScript 1.7.48 310 | - New API to access context: `child-context`, `child-context-types`, `context-types` (thx [Karanbir Toor](https://github.com/currentoor), [#37](https://github.com/tonsky/rum/pull/37)) 311 | - New `defcc` macro for when you only need React component, not the whole Rum state 312 | - [ BREAKING ] Component inner state (`:rum/state`) was moved from `props` to `state`. It doesn’t change a thing if you were using Rum API only, but might break something if you were relaying on internal details 313 | - Deprecated `rum/with-props` macro, use `rum/with-key` or `rum/with-ref` fns instead 314 | 315 | ## 0.2.7 316 | 317 | - Allow components to refer to themselves (thx [Kevin Lynagh](https://github.com/lynaghk), [#30](https://github.com/tonsky/rum/pull/30)) 318 | - Support for multi-arity render fns ([#23](https://github.com/tonsky/rum/issues/23)) 319 | 320 | ## 0.2.6 321 | 322 | - Added `local` mixin 323 | 324 | ## 0.2.5 325 | 326 | - Fixed argument destructuring in defc macro ([#22](https://github.com/tonsky/rum/issues/22)) 327 | 328 | ## 0.2.4 329 | 330 | - `will-update` and `did-update` lifecycle methods added (thx [Andrey Vasenin](https://github.com/avasenin), [#18](https://github.com/tonsky/rum/pull/18)) 331 | 332 | ## 0.2.3 333 | 334 | - Components defined via `defc/defcs` will have `displayName` defined (thx [Ivan Dubrov](https://github.com/idubrov), [#16](https://github.com/tonsky/rum/pull/16)) 335 | - Not referencing `requestAnimationFrame` when used in headless environment (thx @[whodidthis](https://github.com/whodidthis), [#14](https://github.com/tonsky/rum/pull/14)) 336 | 337 | ## 0.2.2 338 | 339 | - Compatibility with clojurescript 0.0-2758, macros included automatically when `(:require rum)` 340 | 341 | ## 0.2.1 342 | 343 | - Updated deps to clojurescript 0.0-2727, react 0.12.2-5 and sablono 0.3.1 344 | 345 | ## 0.2.0 346 | 347 | - [ BREAKING ] New syntax for mixins: `(defc name < mixin1 mixin2 [args] body...)` 348 | - New `defcs` macro that adds additional first argument to render function: `state` 349 | - Ability to specify `key` and `ref` to rum components via `with-props` 350 | 351 | ## 0.1.1 352 | 353 | - Fixed a bug when render-loop tried to `.forceUpdate` unmounted elements 354 | - Fixed a cursor leak bug in `reactive` mixin 355 | - Removed `:should-update` from `reactive`, it now will be re-rendered if re-created by top-level element 356 | - Combine `reactive` with `static` to avoid re-rendering if component is being recreated with the same args 357 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Eclipse Public License - v 1.0 2 | 3 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC 4 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM 5 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 6 | 7 | 1. DEFINITIONS 8 | 9 | "Contribution" means: 10 | 11 | a) in the case of the initial Contributor, the initial code and documentation 12 | distributed under this Agreement, and 13 | b) in the case of each subsequent Contributor: 14 | i) changes to the Program, and 15 | ii) additions to the Program; 16 | 17 | where such changes and/or additions to the Program originate from and are 18 | distributed by that particular Contributor. A Contribution 'originates' 19 | from a Contributor if it was added to the Program by such Contributor 20 | itself or anyone acting on such Contributor's behalf. Contributions do not 21 | include additions to the Program which: (i) are separate modules of 22 | software distributed in conjunction with the Program under their own 23 | license agreement, and (ii) are not derivative works of the Program. 24 | 25 | "Contributor" means any person or entity that distributes the Program. 26 | 27 | "Licensed Patents" mean patent claims licensable by a Contributor which are 28 | necessarily infringed by the use or sale of its Contribution alone or when 29 | combined with the Program. 30 | 31 | "Program" means the Contributions distributed in accordance with this 32 | Agreement. 33 | 34 | "Recipient" means anyone who receives the Program under this Agreement, 35 | including all Contributors. 36 | 37 | 2. GRANT OF RIGHTS 38 | a) Subject to the terms of this Agreement, each Contributor hereby grants 39 | Recipient a non-exclusive, worldwide, royalty-free copyright license to 40 | reproduce, prepare derivative works of, publicly display, publicly 41 | perform, distribute and sublicense the Contribution of such Contributor, 42 | if any, and such derivative works, in source code and object code form. 43 | b) Subject to the terms of this Agreement, each Contributor hereby grants 44 | Recipient a non-exclusive, worldwide, royalty-free patent license under 45 | Licensed Patents to make, use, sell, offer to sell, import and otherwise 46 | transfer the Contribution of such Contributor, if any, in source code and 47 | object code form. This patent license shall apply to the combination of 48 | the Contribution and the Program if, at the time the Contribution is 49 | added by the Contributor, such addition of the Contribution causes such 50 | combination to be covered by the Licensed Patents. The patent license 51 | shall not apply to any other combinations which include the Contribution. 52 | No hardware per se is licensed hereunder. 53 | c) Recipient understands that although each Contributor grants the licenses 54 | to its Contributions set forth herein, no assurances are provided by any 55 | Contributor that the Program does not infringe the patent or other 56 | intellectual property rights of any other entity. Each Contributor 57 | disclaims any liability to Recipient for claims brought by any other 58 | entity based on infringement of intellectual property rights or 59 | otherwise. As a condition to exercising the rights and licenses granted 60 | hereunder, each Recipient hereby assumes sole responsibility to secure 61 | any other intellectual property rights needed, if any. For example, if a 62 | third party patent license is required to allow Recipient to distribute 63 | the Program, it is Recipient's responsibility to acquire that license 64 | before distributing the Program. 65 | d) Each Contributor represents that to its knowledge it has sufficient 66 | copyright rights in its Contribution, if any, to grant the copyright 67 | license set forth in this Agreement. 68 | 69 | 3. REQUIREMENTS 70 | 71 | A Contributor may choose to distribute the Program in object code form under 72 | its own license agreement, provided that: 73 | 74 | a) it complies with the terms and conditions of this Agreement; and 75 | b) its license agreement: 76 | i) effectively disclaims on behalf of all Contributors all warranties 77 | and conditions, express and implied, including warranties or 78 | conditions of title and non-infringement, and implied warranties or 79 | conditions of merchantability and fitness for a particular purpose; 80 | ii) effectively excludes on behalf of all Contributors all liability for 81 | damages, including direct, indirect, special, incidental and 82 | consequential damages, such as lost profits; 83 | iii) states that any provisions which differ from this Agreement are 84 | offered by that Contributor alone and not by any other party; and 85 | iv) states that source code for the Program is available from such 86 | Contributor, and informs licensees how to obtain it in a reasonable 87 | manner on or through a medium customarily used for software exchange. 88 | 89 | When the Program is made available in source code form: 90 | 91 | a) it must be made available under this Agreement; and 92 | b) a copy of this Agreement must be included with each copy of the Program. 93 | Contributors may not remove or alter any copyright notices contained 94 | within the Program. 95 | 96 | Each Contributor must identify itself as the originator of its Contribution, 97 | if 98 | any, in a manner that reasonably allows subsequent Recipients to identify the 99 | originator of the Contribution. 100 | 101 | 4. COMMERCIAL DISTRIBUTION 102 | 103 | Commercial distributors of software may accept certain responsibilities with 104 | respect to end users, business partners and the like. While this license is 105 | intended to facilitate the commercial use of the Program, the Contributor who 106 | includes the Program in a commercial product offering should do so in a manner 107 | which does not create potential liability for other Contributors. Therefore, 108 | if a Contributor includes the Program in a commercial product offering, such 109 | Contributor ("Commercial Contributor") hereby agrees to defend and indemnify 110 | every other Contributor ("Indemnified Contributor") against any losses, 111 | damages and costs (collectively "Losses") arising from claims, lawsuits and 112 | other legal actions brought by a third party against the Indemnified 113 | Contributor to the extent caused by the acts or omissions of such Commercial 114 | Contributor in connection with its distribution of the Program in a commercial 115 | product offering. The obligations in this section do not apply to any claims 116 | or Losses relating to any actual or alleged intellectual property 117 | infringement. In order to qualify, an Indemnified Contributor must: 118 | a) promptly notify the Commercial Contributor in writing of such claim, and 119 | b) allow the Commercial Contributor to control, and cooperate with the 120 | Commercial Contributor in, the defense and any related settlement 121 | negotiations. The Indemnified Contributor may participate in any such claim at 122 | its own expense. 123 | 124 | For example, a Contributor might include the Program in a commercial product 125 | offering, Product X. That Contributor is then a Commercial Contributor. If 126 | that Commercial Contributor then makes performance claims, or offers 127 | warranties related to Product X, those performance claims and warranties are 128 | such Commercial Contributor's responsibility alone. Under this section, the 129 | Commercial Contributor would have to defend claims against the other 130 | Contributors related to those performance claims and warranties, and if a 131 | court requires any other Contributor to pay any damages as a result, the 132 | Commercial Contributor must pay those damages. 133 | 134 | 5. NO WARRANTY 135 | 136 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN 137 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR 138 | IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each 140 | Recipient is solely responsible for determining the appropriateness of using 141 | and distributing the Program and assumes all risks associated with its 142 | exercise of rights under this Agreement , including but not limited to the 143 | risks and costs of program errors, compliance with applicable laws, damage to 144 | or loss of data, programs or equipment, and unavailability or interruption of 145 | operations. 146 | 147 | 6. DISCLAIMER OF LIABILITY 148 | 149 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY 150 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, 151 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION 152 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 153 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 154 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 155 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY 156 | OF SUCH DAMAGES. 157 | 158 | 7. GENERAL 159 | 160 | If any provision of this Agreement is invalid or unenforceable under 161 | applicable law, it shall not affect the validity or enforceability of the 162 | remainder of the terms of this Agreement, and without further action by the 163 | parties hereto, such provision shall be reformed to the minimum extent 164 | necessary to make such provision valid and enforceable. 165 | 166 | If Recipient institutes patent litigation against any entity (including a 167 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself 168 | (excluding combinations of the Program with other software or hardware) 169 | infringes such Recipient's patent(s), then such Recipient's rights granted 170 | under Section 2(b) shall terminate as of the date such litigation is filed. 171 | 172 | All Recipient's rights under this Agreement shall terminate if it fails to 173 | comply with any of the material terms or conditions of this Agreement and does 174 | not cure such failure in a reasonable period of time after becoming aware of 175 | such noncompliance. If all Recipient's rights under this Agreement terminate, 176 | Recipient agrees to cease use and distribution of the Program as soon as 177 | reasonably practicable. However, Recipient's obligations under this Agreement 178 | and any licenses granted by Recipient relating to the Program shall continue 179 | and survive. 180 | 181 | Everyone is permitted to copy and distribute copies of this Agreement, but in 182 | order to avoid inconsistency the Agreement is copyrighted and may only be 183 | modified in the following manner. The Agreement Steward reserves the right to 184 | publish new versions (including revisions) of this Agreement from time to 185 | time. No one other than the Agreement Steward has the right to modify this 186 | Agreement. The Eclipse Foundation is the initial Agreement Steward. The 187 | Eclipse Foundation may assign the responsibility to serve as the Agreement 188 | Steward to a suitable separate entity. Each new version of the Agreement will 189 | be given a distinguishing version number. The Program (including 190 | Contributions) may always be distributed subject to the version of the 191 | Agreement under which it was received. In addition, after a new version of the 192 | Agreement is published, Contributor may elect to distribute the Program 193 | (including its Contributions) under the new version. Except as expressly 194 | stated in Sections 2(a) and 2(b) above, Recipient receives no rights or 195 | licenses to the intellectual property of any Contributor under this Agreement, 196 | whether expressly, by implication, estoppel or otherwise. All rights in the 197 | Program not expressly granted under this Agreement are reserved. 198 | 199 | This Agreement is governed by the laws of the State of New York and the 200 | intellectual property laws of the United States of America. No party to this 201 | Agreement will bring a legal action under this Agreement more than one year 202 | after the cause of action arose. Each party waives its rights to a jury trial in 203 | any resulting litigation. 204 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:deps {org.clojure/clojure {:mvn/version "1.9.0"} 2 | org.clojure/clojurescript {:mvn/version "1.10.773"} 3 | cljsjs/react {:mvn/version "16.8.6-0"} 4 | cljsjs/react-dom {:mvn/version "16.8.6-0"}} 5 | :aliases {:test {:extra-paths ["test"]}}} 6 | -------------------------------------------------------------------------------- /doc/cljdoc.edn: -------------------------------------------------------------------------------- 1 | {:cljdoc.doc/tree [ 2 | ["Readme" {:file "README.md"}] 3 | ["Changelog" {:file "CHANGELOG.md"}] 4 | ["Useful mixins" {:file "doc/useful-mixins.md"}] 5 | ["React interop" {:file "doc/react-interop.md"}] 6 | ["React Suspense and code-splitting" {:file "doc/react-suspense-and-code-splitting.md"}] 7 | ["React Hooks" {:file "doc/react-hooks.md"}] 8 | ]} 9 | -------------------------------------------------------------------------------- /doc/optimizations.md: -------------------------------------------------------------------------------- 1 | # Optimizations 2 | 3 | ## When to use `rum/static` mixin? 4 | 5 | `rum/static` applies `shouldComponentUpdate` or `React.memo`, in case of hooks-based components, optimization to underlying React component. The role of this optimization is to check if component's arguments are different from ones passed-in in the previous render of the component. If they didn't change, then React will skip running the component and will reuse returned value from previous render. In other words it's a memoization of React components based on the arguments from the most recent call to a component. 6 | 7 | It's tempting to apply this optimization to every single component when using React in ClojureScript, because we know that equality check on immutable data is free. 8 | 9 | Unfortunately this is not entirely true. Understanding this is important when making a decision to optimize particular component. 10 | 11 | Equality check on immutable data is fast when it falls into identity check, for example `=` operation in this case will short-circuit on `identical?` check (which is performed inside of `=`). 12 | ```clojure 13 | (def x {:key :value}) 14 | 15 | (= x x) 16 | ``` 17 | 18 | On the other hand comparing two values created from scratch will perform value equality operation (deep equals), which walks both entire data structures to figure out if they are equal. 19 | ```clojure 20 | (= {:key :value} {:key :value}) 21 | ``` 22 | 23 | ### When `=` is fast? 24 | 25 | Structural sharing is what makes equality check fast on immutable data structures. This obviously doesn't apply to values created from scratch, as in the example above, because they don't have shared structure. 26 | 27 | But updating a value and comparing the result to the original value will be faster. 28 | ```clojure 29 | (def x {:x {:a 1 :b 2} 30 | :y {:c 3 :d 4}}) 31 | 32 | (def y (update-in x [:x :a] inc)) 33 | 34 | (= x y) 35 | ``` 36 | In the above case `x` and `y` share `:y {:c 3 :d 4}` part, which will be checked only with `identitcal?` and then `:x {:a 1 :b 2}` will be traversed fully, since this path is updated in `y` and thus not `identitcal?` anymore. 37 | 38 | In the context of React, patterns with central data store, such as re-frame, benefit from structural sharing, because re-frame is updating subtrees of the original value. Taking those subtrees from central data store and passing them into memoized React components will result in an efficient memoization, where most of the time equality check will be short-circuited with `identical?` call. 39 | 40 | But creating data locally in a component, such as a hash map of attributes, means that on every run of a component those values has to be compared by value (deep equals), because they are created from scratch (no shared structure). 41 | 42 | How to use this information now? The thing is that sometimes running a component again would be cheaper than comparing its arguments, especially for components that are frequently updated with a different set of values. That also depends on React wrapper library that you are using. For example in Reagent, where Hiccup is interpreted at runtime it's very likely might be the case that memoizing a component would be cheaper than running it, since Hiccup has to be transformed into `React.createElement` calls. In Rum for example, where most of the Hiccup is pre-compiled into React calls via `defc` macro the perf hit is lower than in Reagent, thus the possibility of usefulness of memoization is lower. 43 | -------------------------------------------------------------------------------- /doc/react-hooks.md: -------------------------------------------------------------------------------- 1 | # React Hooks 2 | 3 | Starting from `0.11.5` Rum provides wrappers for a set of React Hooks that can be used from Rum components. 4 | 5 | ## Limitations 6 | 7 | First things first, hooks can be used only inside of `defc` components with optional `rum/static` mixin that enables component's memoization based on its arguments. The reason for that is that React Hooks work only in function-based components and Rum generates those for `defc` components that are not using mixins, because mixins are meant to be executed in lifecycle methods of class-based components. With that make sure that you are not using both hooks and mixins in a single component, otherwise Rum fallsback to generating class-based components and React will throw an expection about incorrect usage of hooks. 8 | 9 | ## About rum/static 10 | 11 | `rum/static` is a mixin that enables component's memoization based on arguments. In class-based components it declares `shoudComponentUpdate` method that compares previous and new arguments. When used as the only mixin in `defc` we generate function-based component and use `rum/static` as a compile-time marker to generate `React.memo` wrapper for a component. `React.memo` is the same as `shoudComponentUpdate`, but meant to be used with function-based components. 12 | 13 | ## Local state hook 14 | 15 | `rum/use-state` takes initial state value and returns a tuple, where the first entry is current state and the second one is a function that takes a new state and schedules an update of the component. It's important to understand that the update is scheduled, which means that the component will be re-rendered eventually, not syncronously. Additionally instead of initla value the hook can take a function that computes initial state, so that the initial value won't be recomputed on every update, but only once, when component is instantiated. 16 | 17 | ```clojure 18 | (rum/defc input-field [] 19 | (let [[value set-value!] (rum/use-state "")] 20 | [:input {:value value 21 | :on-change #(set-value! (.. % -target -value))}])) 22 | ``` 23 | 24 | ## Effect hook 25 | 26 | `rum/use-effect!` can be thought of as a replacement of `:did-mount`, `:will-unmount` and `:did-update` mixins. The hook takes a setup function which optionally can return a cleanup function. The former is meant to be used for side-effects execution and the latter to cleanup the result of that operation, if needed. 27 | 28 | In this example the component will setup a global `keydown` handler after every update and remove the handler right before every update. This makes sense in cases when the handler can schedule another update for example by updating local state, so we don't want this to happen when the component is already in update phase. 29 | 30 | ```clojure 31 | (rum/defc input-field [] 32 | (rum/use-effect! 33 | (fn [] 34 | (let [handler #(println :key (.-key %))] 35 | (.addEventListener js/document "keydown" handler) 36 | #(.removeEventListener js/document "keydown" handler)))) 37 | ...) 38 | ``` 39 | 40 | But when you only want to setup event listener once, when component is instantiated and remove it right before component gets removed from UI tree you should use the second argument to the hook, which is a collection of dependencies. Dependencies should be used for conditional execution of hooks. If previous deps are different from new ones, after an update, then the hook will re-execute. In case when we want it to execute only once, on mount and before unmount, deps collection should be an empty collection, just `[]`. 41 | 42 | ## Limitation of dependencies collection 43 | 44 | While the collection itself can be either JS Array or Clojure's Vector etc. the entries will be always compared by identity `identical?`, not by value as you would usually expect this in Clojure. The reason for that is that the eqaulity check is performed on React's side and the API doesn't have a way to provide a custom comparator for us. 45 | 46 | ## Callback caching hook 47 | 48 | `rum/use-callback` caches a callback function based on provided dependencies. In cases when you have a parent component with local state that gives control of updating it to child components via passed callback function child components will be updated every time the parent component updates, even though child components are memoized with `rum/static` mixin. That happens because a callback function is re-created on every update, whicn invalidates memoize child components. The hook is able to cache the callback so that it's not re-created unless the dependencies change. 49 | 50 | ```clojure 51 | (rum/defc list-item* < rum/static [on-click] 52 | ...) 53 | 54 | (rum/defc list-item [idx on-click] 55 | (let [handle-click (rum/use-callback #(on-click idx %) [idx]) 56 | [list-item* handle-click]])) 57 | ``` 58 | 59 | ## Memoization hook 60 | 61 | `rum/use-memo` is meant to be used for caching values and expensive computations, again re-evalution is controlled by deps collection. In the example below the component caches an instance of a class from some JavaScript library so that it's not re-instantiated on every update even though `config` doesn't change. 62 | 63 | ```clojure 64 | (rum/defc component [config] 65 | (let [js-lib (rum/use-memo #(js/LibClass. config) [config])] 66 | ...)) 67 | ``` 68 | -------------------------------------------------------------------------------- /doc/react-interop.md: -------------------------------------------------------------------------------- 1 | # Use different React version 2 | 3 | Add to `project.clj`: 4 | 5 | ``` 6 | :dependencies { 7 | [rum "0.11.3" :exclusions [[cljsjs/react] [cljsjs/react-dom]]] 8 | [cljsjs/react "16.6.0-0"] 9 | [cljsjs/react-dom "16.6.0-0"] 10 | } 11 | ``` 12 | 13 | # Including React.js manually 14 | 15 | If you want to include `react.js` yourself, then add this to `project.clj`: 16 | 17 | ``` 18 | :dependencies { 19 | [rum "0.11.3" :exclusions [[cljsjs/react] [cljsjs/react-dom]]] 20 | } 21 | ``` 22 | 23 | Create two files 24 | 25 | 1. `src/cljsjs/react.cljs`: 26 | 27 | ``` 28 | (ns cljsjs.react) 29 | ``` 30 | 31 | 2. `src/cljsjs/react/dom.cljs`: 32 | 33 | ``` 34 | (ns cljsjs.react.dom) 35 | ``` 36 | 37 | Add to your HTML the version you want: 38 | 39 | ``` 40 | 41 | 42 | ``` 43 | 44 | # Using React with addons 45 | 46 | ```clj 47 | [rum "0.11.3" :exclusions [cljsjs/react cljsjs/react-dom]] 48 | [cljsjs/react-dom "16.2.0-3" :exclusions [cljsjs/react]] 49 | [cljsjs/react-dom-server "16.2.0-3" :exclusions [cljsjs/react]] 50 | [cljsjs/react-with-addons "16.2.0-3"] 51 | ``` 52 | 53 | # Profiling with [React perf](https://facebook.github.io/react/docs/perf.html) 54 | 55 | Specify the `react-with-addons` dependency in your `project.clj` (see above ↑) 56 | 57 | Then from within your program run: 58 | 59 | ```clj 60 | (js/React.addons.Perf.start) 61 | ;;run your app 62 | (js/React.addons.Perf.stop) 63 | (js/React.addons.Perf.printWasted) 64 | ``` 65 | 66 | and results will be printed to the developer console. 67 | 68 | # Using 3rd-party React components 69 | 70 | ## via Adapter API 71 | 72 | Since `0.11.5` Rum provides an API to adapt React component for usage in Rum components. But you can still create components manually as described in the next section. 73 | 74 | ```clojure 75 | (rum/defc component [] 76 | [:div 77 | (rum/adapt-class js/Slider 78 | {:min min 79 | :max max 80 | :range true 81 | :defaultValue [40 60]})]) 82 | ``` 83 | 84 | When rendering on JVM Rum browsides a hook to fallback rendering of JS components, so you can delegate this work to JS environment such as GraalJS or renderer a placeholder instead. 85 | 86 | ```clojure 87 | (defn render-js-component [type-sym attrs children] 88 | (case type-sym 89 | 'js/Slider (rum/render-static-markup (slider-placeholder)) 90 | nil)) 91 | 92 | (binding [rum/*render-js-component* render-js-component] 93 | (component)) 94 | ``` 95 | 96 | ## via React.js directly 97 | 98 | Given e.g. [react-router-transition](https://github.com/maisano/react-router-transition) 99 | 100 | ```clj 101 | (defn route-transition [pathname children] 102 | (js/React.createElement js/RouteTransition 103 | #js { :pathname pathname 104 | :atEnter #js { :opacity 0 } 105 | :atLeave #js { :opacity 0 } 106 | :atActive #js { :opacity 1 } } 107 | (clj->js children))) 108 | ``` 109 | 110 | Another example [react-component/slider](https://github.com/react-component/slider) 111 | 112 | ```clj 113 | (defn range-slider [min max] 114 | (js/React.createElement js/Slider #js { :min min 115 | :max max 116 | :range true 117 | :defaultValue #js [40 60] })) 118 | ``` 119 | 120 | If you want to mix 3rd-party React components with child elements using the Hiccup-like syntax, you can call directly into the library that provides it, daiquiri. This can be particularly useful for 3rd-party React components that are made to wrap your own components, like drag-and-drop plugins and so on. 121 | 122 | ```clj 123 | (js/React.createElement js/MyComponent 124 | #js { } 125 | (daiquiri.core/html [:div [:p "Hello, world"]])) 126 | ``` 127 | 128 | **Note:** See how `defn` is used here instead of `defc`? Using `defc` would cause two components being created (e.g. `range-slider` and the `Slider` component). Because in many cases you don't need the wrapping component you can just use `defn`. 129 | 130 | # Using Rum component in React 131 | 132 | In order to use Rum component from React JS code you have to create gluing layer in a form of wrapping function (React component) that unwraps React's props object and passes values into Rum component as arguments. 133 | 134 | ```clojure 135 | (rum/defc button [{:keys [on-click]} text] 136 | [:button {:on-click on-click} 137 | text]) 138 | 139 | (defn Button [^js props] 140 | (button {:on-click (.-onClick props)} 141 | (.-children props))) 142 | ``` 143 | 144 | # Get displayName of component 145 | 146 | This might be useful for development when you want to know which component this mixin is handling: 147 | 148 | ```clojure 149 | (ns ... 150 | (:require [goog.object :as gobj])) 151 | 152 | (defn display-name 153 | "Returns the displayname of the component" 154 | [state] 155 | (gobj/getValueByKeys 156 | (:rum/react-component state) 157 | "constructor" 158 | "displayName")) 159 | ``` 160 | -------------------------------------------------------------------------------- /doc/react-suspense-and-code-splitting.md: -------------------------------------------------------------------------------- 1 | # React Suspense and code-splitting 2 | 3 | > This requires Rum 0.11.5-SNAPSHOT or newer 4 | 5 | [React Suspense](https://reactjs.org/docs/code-splitting.html) allows loading components lazily from code chunks while displaying a fallback UI when a chunk is loading. 6 | 7 | Create build configuration in `build.edn`. `:modules` describe code chunks with their corresponding entry points, read more about code-splitting at [https://clojurescript.org/guides/code-splitting](https://clojurescript.org/guides/code-splitting) 8 | 9 | ```clojure 10 | {:output-dir "resources/public/out" 11 | :asset-path "out" 12 | :optimizations :advanced 13 | :modules {:core 14 | {:entries #{example.core} 15 | :output-to "resources/public/out/core.js"} 16 | :components 17 | {:entries #{example.components} 18 | :output-to "resources/public/out/components.js"}}} 19 | ``` 20 | 21 | In `example.components` namespace we declare a component that will be used later in `example.core` ns, which will be in another chunk. For that to work we have to instruct explicitly chunks loader runtime that it was loaded. `cljs.loader/set-loaded!` takes the name of the chunk as specified in build config. 22 | 23 | ```clojure 24 | (ns example.components 25 | (:require [rum.core :as rum] 26 | [cljs.loader :as loader])) 27 | 28 | (rum/defc alert [arg] 29 | [:h1 arg]) 30 | 31 | (loader/set-loaded! :components) 32 | ``` 33 | 34 | In `example.core` we do the same to instruct about when chunk loading is done, but we also using `require-lazy` macro to require `alert` component from a namespace in another chunk. Then the component is wrapped in `suspense` component that takes care of loading and displaying a fallback. 35 | 36 | ```clojure 37 | (ns example.core 38 | (:require [cljs.loader :as loader] 39 | [rum.core :as rum] 40 | [rum.lazy-loader :refer [require-lazy]])) 41 | 42 | (require-lazy '[example.components :refer [alert]]) 43 | 44 | (rum/defc root [] 45 | (rum/suspense {:fallback "Hello!"} 46 | (alert "ARGUMENT"))) 47 | 48 | (loader/set-loaded! :core) 49 | 50 | (rum/mount (root) (.getElementById js/document "root")) 51 | ``` 52 | 53 | Now if you build the code `clj -m cljs.main -co build.edn -c example.core` you'll get 3 chunks: `core.js`, `components.js` and `cljs_base.js` which includes the code shared between those two chunks. 54 | 55 | Your HTML should look like this. 56 | 57 | ```html 58 |
59 | 60 | 61 | ``` 62 | -------------------------------------------------------------------------------- /doc/useful-mixins.md: -------------------------------------------------------------------------------- 1 | # Request stuff on mount by AJAX 2 | 3 | Components using this mixin will do an AJAX request on mount and will update themselves when they got a reply. Mixin puts an atom to the state whose value (after deref) is either nil (request pending) or returned value. 4 | 5 | ```clojure 6 | (defn ajax-mixin [url key] 7 | { :will-mount 8 | (fn [state] 9 | (let [*data (atom nil) 10 | comp (:rum/react-component state)] 11 | (ajax 12 | url 13 | (fn [data] 14 | (reset! *data data) 15 | (rum/request-render comp))) 16 | (assoc state key *data))) }) 17 | 18 | 19 | (rum/defcs user-info < (ajax-mixin "/api/user/info" ::user) 20 | [state] 21 | (if-let [user @(::user state)] 22 | ... 23 | [:div "Loading..."])) 24 | ``` 25 | 26 | Customize to your taste: dynamic URL generation from component args, AJAX retries, failed state, callback on finish, deserialization. 27 | 28 | # Debouncer 29 | 30 | ```clojure 31 | (ns ... (:import [goog.async Debouncer])) 32 | 33 | (defn debouncer-mixin 34 | "Creates a debouncer in (:debouncer state) which can be called with (.fire). 35 | Invokes the callback cb or invokes the first argument passed to fire. 36 | Usage: 37 | 1. (debouncer-mixin 200) 38 | (.fire (:debouncer state) #(do-actual-action ...)) 39 | 40 | 2. (debouncer-mixin 200 #(do-an-action ...)) 41 | (.fire (:debouncer state))" 42 | ([ms] (debouncer-mixin ms nil)) 43 | ([ms cb] 44 | {:will-mount 45 | (fn debouncer-mount [state] 46 | (assoc state :debouncer (Debouncer. (if (nil? cb) #(%1) cb) ms))) 47 | :will-unmount 48 | (fn debouncer-unmount [state] 49 | (.dispose (:debouncer state)) 50 | state)})) 51 | ``` 52 | 53 | # Install CSS styles on mount 54 | 55 | For your root rum component, you can install CSS styles on mount and uninstall them on unmount. 56 | This is useful if you have your (garden) styles defined in `cljc` file. In production you generate 57 | a css file and include it with a normal `
Timers
Static: 00:00:00.000
Reactive: 00:00:00.000
Controls
Color:
Clone:
Color:
0 watches
Tick:
ms
Time:
0 watches
Reactive binary clock
000000000
Renders: 32
Reactive artboard
Renders: 0
Board watches: 0
Color watches: 0
BMI Calculator
Height: 180cm
Weight: 80kg
BMI: 24 normal
Form validation
Inputs
Input
Checks
Radio
Select
1
Refs
Local state
Clicks count: 0
Keys
a-1
a-2
b-1
a-1
Self-reference
:a
:b
:c
:d
:e
:g
Contexts
Custom Methods and Data
Multiple Return
Portals
Error boundaries

Server: CAUGHT: clojure.lang.ExceptionInfo: render error {}

Client:

JavaScript components

This is Rum component

This is JS component

-------------------------------------------------------------------------------- /perf/pages/page1.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 9 | 10 |
11 |

2016

12 |

The Blessing of Interactive Development 4/11 13 |

2015

14 |

The Web After Tomorrow 6/23 15 |

A shallow dive into DataScript internals 2/23 16 |

2014

17 |

Couple of DataScript resources 12/18 18 |

Streams: Mail 3.0 concept 10/27 19 |

Another powered-by-DataScript example 10/6 20 |

Chatting cats use DataScript for fun 9/18 21 |

Irrelevant Things 8/12 22 |

Reinventing Git interface 6/17 23 |

Unofficial guide to Datomic internals 5/6 24 |

Datomic as a Protocol 4/29 25 |

Decomposing web app development 4/24 26 | 27 | 28 | -------------------------------------------------------------------------------- /perf/rum/perf.clj: -------------------------------------------------------------------------------- 1 | (ns rum.perf 2 | (:require 3 | [rum.core :as rum] 4 | [clojure.string :as str] 5 | [net.cgrand.enlive-html :as enlive] 6 | [clojure.test :refer [deftest]] 7 | [criterium.core :as criterium] 8 | [hiccup.core :as hiccup])) 9 | 10 | 11 | (def ^:dynamic *convert-style?* true) 12 | 13 | 14 | (defn convert-tag-name [tag attrs] 15 | (let [id (:id attrs) 16 | classes (when-not (str/blank? (:class attrs)) 17 | (->> (str/split (:class attrs) #"\s+") 18 | (remove str/blank?)))] 19 | (keyword 20 | (str tag 21 | (when id (str "#" id)) 22 | (when-not (empty? classes) 23 | (str "." (str/join "." classes))))))) 24 | 25 | 26 | (defn convert-style [s] 27 | (into {} 28 | (for [[_ k v] (re-seq #"([\w+\-]+)\s*:\s*([^;]+)" s)] 29 | (let [k' (keyword k) 30 | v' (condp re-matches v 31 | #"(\d+)px" :>> (fn [[_ n]] (Long/parseLong n)) 32 | #"(\d+\.\d+)px" :>> (fn [[_ n]] (Double/parseDouble n)) 33 | v)] 34 | [k' v'])))) 35 | 36 | 37 | (defn convert-attrs [attrs] 38 | (cond-> attrs 39 | true (dissoc :class :id :data-bem) 40 | (and *convert-style?* 41 | (contains? attrs :style)) (update :style convert-style) 42 | true not-empty)) 43 | 44 | 45 | (defn convert-tag [form] 46 | (cond 47 | ;; tag 48 | (map? form) 49 | (if (= :comment (:type form)) 50 | nil 51 | (let [{:keys [tag attrs content type]} form 52 | tag' (convert-tag-name (name tag) attrs) 53 | attrs' (convert-attrs attrs) 54 | children (->> (map convert-tag content) 55 | (remove nil?))] 56 | (vec 57 | (concat [tag'] (when attrs' [attrs']) children)))) 58 | 59 | ;; text node 60 | (string? form) 61 | (if (str/blank? form) nil form))) 62 | 63 | 64 | (defn convert-page [page] 65 | (-> (slurp page) 66 | .getBytes 67 | java.io.ByteArrayInputStream. 68 | enlive/html-resource 69 | (enlive/select [:body]) 70 | first 71 | convert-tag)) 72 | 73 | 74 | (defn file-size [path] 75 | (-> (count (slurp path)) (/ 1000) (long) (str " kB"))) 76 | 77 | 78 | (defn -main [& args] 79 | (doseq [page ["page1.html" 80 | "page2.html" 81 | "page3.html"] 82 | :let [path (str "perf/pages/" page)]] 83 | (let [comp (convert-page path)] 84 | (println "\n--- Testing" page (str "(" (file-size path) ")") "---") 85 | (criterium/quick-bench (rum/render-static-markup comp))) 86 | 87 | (let [comp (binding [*convert-style?* false] 88 | (convert-page path))] 89 | (println "\n+++ With Hiccup +++") 90 | (criterium/quick-bench (hiccup/html comp))))) 91 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject rum "0.12.11" 2 | :description "ClojureScript wrapper for React" 3 | :license {:name "Eclipse" 4 | :url "http://www.eclipse.org/legal/epl-v10.html"} 5 | :url "https://github.com/tonsky/rum" 6 | 7 | :dependencies 8 | [[org.clojure/clojure "1.9.0" :scope "provided"] 9 | [org.clojure/clojurescript "1.10.773" :scope "provided"] 10 | [cljsjs/react "16.8.6-0"] 11 | [cljsjs/react-dom "16.8.6-0"]] 12 | 13 | :plugins [[lein-cljsbuild "1.1.7"] 14 | [lein-cljfmt "0.6.7"]] 15 | 16 | :profiles {:dev {:source-paths ["examples"] 17 | :dependencies [[cljsjs/react-dom-server "16.8.6-0"] 18 | [cljsjs/prop-types "15.7.2-0"] 19 | [clj-diffmatchpatch "0.0.9.3" :exclusions [org.clojure/clojure]]]} 20 | :perf {:source-paths ["perf"] 21 | :dependencies 22 | [[enlive "1.1.6"] 23 | [criterium "0.4.4"] 24 | [hiccup "1.0.5"]]}} 25 | 26 | :aliases {"package" ["do" ["clean"] ["test"] ["clean"] ["cljsbuild" "once" "advanced"] ["run" "-m" "rum.examples-page"]] 27 | "perf" ["with-profile" "perf" "run" "-m" "rum.perf"]} 28 | 29 | 30 | :cljsbuild 31 | {:builds 32 | [{:id "advanced" 33 | :source-paths ["src" "examples" "test"] 34 | :compiler 35 | {:main rum.examples 36 | :output-to "target/main.js" 37 | :optimizations :advanced 38 | :source-map "target/main.js.map" 39 | :pretty-print false 40 | :compiler-stats true 41 | :parallel-build true}} 42 | 43 | {:id "none" 44 | :source-paths ["src" "examples" "test"] 45 | :compiler 46 | {:main rum.examples 47 | :output-to "target/main.js" 48 | :output-dir "target/none" 49 | :asset-path "target/none" 50 | :optimizations :none 51 | :source-map true 52 | :compiler-stats true 53 | :parallel-build true}} 54 | 55 | {:id "test" 56 | :source-paths ["src" "test"] 57 | :compiler 58 | {:main rum.test.server-render 59 | :output-to "target/test.js" 60 | :output-dir "target/test" 61 | :asset-path "target/test" 62 | :optimizations :advanced 63 | :pretty-print true 64 | :pseudo-names true 65 | :parallel-build true}}]}) 66 | 67 | -------------------------------------------------------------------------------- /scripts/test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | set -o pipefail 5 | 6 | lein test 7 | 8 | echo "Building ClojureScript unit tests..." 9 | clojure -A:test -m cljs.main -co '{:target :nodejs :optimizations :none :output-to "test.js"}' -c test-runner 10 | 11 | echo "Running ClojureScript unit tests..." 12 | node test.js 13 | 14 | rm test.js 15 | -------------------------------------------------------------------------------- /src/daiquiri/compiler.clj: -------------------------------------------------------------------------------- 1 | (ns daiquiri.compiler 2 | (:refer-clojure :exclude [requiring-resolve]) 3 | (:require [daiquiri.normalize :as normalize] 4 | [daiquiri.util :refer :all] 5 | [clojure.set :as set])) 6 | 7 | (def warn-on-interpretation (atom false)) 8 | 9 | (defn- requiring-resolve [sym] 10 | (or (resolve sym) 11 | (do (require (-> sym namespace symbol)) 12 | (resolve sym)))) 13 | 14 | (defn maybe-warn-on-interpret 15 | ([env expr] 16 | (maybe-warn-on-interpret env expr nil)) 17 | ([env expr tag] 18 | (when @warn-on-interpretation 19 | (binding [*out* *err*] 20 | (let [column (:column env) 21 | line (:line env)] 22 | (require 'cljs.analyzer) 23 | (println (str "WARNING: interpreting by default at " (some-> (requiring-resolve 'cljs.analyzer/*cljs-file*) deref) ":" line ":" column)) 24 | (prn expr) 25 | (when tag 26 | (println "Inferred tag was:" tag))))))) 27 | 28 | (def ^:private primitive-types 29 | "The set of primitive types that can be handled by React." 30 | #{'js 'clj-nil 'js/React.Element 31 | 'number 'string 'boolean 'symbol 32 | 'array 'function}) 33 | 34 | (defn- primitive-type? 35 | "Return true if `tag` is a primitive type that can be handled by 36 | React, otherwise false. " 37 | [tags] 38 | (and (not (empty? tags)) 39 | (set/subset? tags primitive-types))) 40 | 41 | (defn- normalize-tags [tags] 42 | (if (set? tags) tags (set [tags]))) 43 | 44 | (defn infer-tag 45 | "Infer the tag of `form` using `env`." 46 | [env form] 47 | (when env 48 | (let [*analyzer-warnings* (requiring-resolve 'cljs.analyzer/*cljs-warnings*) 49 | ;; We would want to use cljs.analyzer/no-warn here but we can't since 50 | ;; it's a macro and won't work with requiring-resolve 51 | e (with-bindings* {*analyzer-warnings* (zipmap (keys @*analyzer-warnings*) (repeat false))} 52 | (fn [] 53 | ((requiring-resolve 'cljs.analyzer/analyze) env form))) 54 | ;; Roman. Propagating Rum's component return tag 55 | ;; via :rum/tag meta field, because a component 56 | ;; is generated as a `def` instead of `defn` 57 | rum-tag (when (= :invoke (:op e)) 58 | (-> e :fn :info :meta :rum/tag second))] 59 | (if rum-tag 60 | (normalize-tags rum-tag) 61 | (when-let [tags ((requiring-resolve 'cljs.analyzer/infer-tag) env e)] 62 | (normalize-tags tags)))))) 63 | 64 | (declare to-js to-js-map) 65 | 66 | (defmulti compile-attr (fn [name value] name)) 67 | 68 | (defmethod compile-attr :class [_ value] 69 | (cond 70 | (or (nil? value) 71 | (keyword? value) 72 | (string? value)) 73 | value 74 | (and (or (sequential? value) 75 | (set? value)) 76 | (every? string? value)) 77 | (join-classes value) 78 | :else `(daiquiri.util/join-classes ~value))) 79 | 80 | (defmethod compile-attr :style [_ value] 81 | (let [value (camel-case-keys value)] 82 | (if (map? value) 83 | (to-js-map value) 84 | `(daiquiri.interpreter/element-attributes ~value)))) 85 | 86 | (defmethod compile-attr :default [_ value] 87 | (to-js value)) 88 | 89 | (defn wrap-event-handlers 90 | "Wrapping on-change handler to work around async rendering queue 91 | that causes jumping caret and lost characters in input fields" 92 | [attrs] 93 | (cond-> attrs 94 | (:on-change attrs) (assoc :on-change `(rum.core/mark-sync-update ~(:on-change attrs))))) 95 | 96 | (defn compile-attrs 97 | "Compile a HTML attribute map." 98 | [attrs] 99 | (when (seq attrs) 100 | (->> (seq attrs) 101 | (reduce (fn [attrs [name value]] 102 | (assoc attrs name (compile-attr name value))) 103 | nil) 104 | wrap-event-handlers 105 | html-to-dom-attrs 106 | to-js-map))) 107 | 108 | (defn compile-merge-attrs [attrs-1 attrs-2] 109 | (let [empty-attrs? #(or (nil? %1) (and (map? %1) (empty? %1)))] 110 | (cond 111 | (and (empty-attrs? attrs-1) 112 | (empty-attrs? attrs-2)) 113 | nil 114 | (empty-attrs? attrs-1) 115 | `(daiquiri.interpreter/element-attributes ~attrs-2) 116 | (empty-attrs? attrs-2) 117 | `(daiquiri.interpreter/element-attributes ~attrs-1) 118 | (and (map? attrs-1) 119 | (map? attrs-2)) 120 | (normalize/merge-with-class attrs-1 attrs-2) 121 | :else `(daiquiri.interpreter/element-attributes 122 | (daiquiri.normalize/merge-with-class ~attrs-1 ~attrs-2))))) 123 | 124 | (defn- compile-tag 125 | "Replace fragment syntax (`:*` or `:<>`) by 'React.Fragment, otherwise the 126 | name of the tag" 127 | [tag] 128 | (if (fragment-tag? tag) 129 | 'daiquiri.core/fragment 130 | (name tag))) 131 | 132 | (declare compile-react) 133 | 134 | (defn compile-react-element 135 | "Render an element vector as a HTML element." 136 | [element env] 137 | (let [[tag attrs content] (normalize/element element)] 138 | `(daiquiri.core/create-element 139 | ~(compile-tag tag) 140 | ~(when (seq attrs) 141 | (compile-attrs attrs)) 142 | ~(when (seq content) 143 | `(cljs.core/array ~@(compile-react content env)))))) 144 | 145 | (defn- unevaluated? 146 | "True if the expression has not been evaluated." 147 | [expr] 148 | (or (symbol? expr) 149 | (and (seq? expr) 150 | (not= (first expr) `quote)))) 151 | 152 | (defmacro interpret-maybe 153 | "Macro that wraps `expr` with a call to 154 | `daiquiri.interpreter/interpret` if the inferred return type is not a 155 | primitive React type." 156 | [expr] 157 | (let [tag (infer-tag &env expr)] 158 | (if (primitive-type? tag) 159 | expr 160 | (do 161 | (maybe-warn-on-interpret &env expr tag) 162 | `(daiquiri.interpreter/interpret ~expr))))) 163 | 164 | (defn- form-name 165 | "Get the name of the supplied form." 166 | [form] 167 | (if (and (seq? form) (symbol? (first form))) 168 | (name (first form)))) 169 | 170 | (declare compile-html) 171 | 172 | (defmulti compile-form 173 | "Pre-compile certain standard forms, where possible." 174 | {:private true} 175 | (fn [form env] (form-name form))) 176 | 177 | (defmethod compile-form "case" 178 | [[_ v & cases] env] 179 | `(case ~v 180 | ~@(doall (mapcat 181 | (fn [[test hiccup]] 182 | (if hiccup 183 | [test (compile-html hiccup env)] 184 | [(compile-html test env)])) 185 | (partition-all 2 cases))))) 186 | 187 | (defmethod compile-form "cond" 188 | [[_ & clauses] env] 189 | `(cond ~@(mapcat 190 | (fn [[check expr]] [check (compile-html expr env)]) 191 | (partition 2 clauses)))) 192 | 193 | (defmethod compile-form "condp" 194 | [[_ f v & cases] env] 195 | `(condp ~f ~v 196 | ~@(doall (mapcat 197 | (fn [[test hiccup]] 198 | (if hiccup 199 | [test (compile-html hiccup env)] 200 | [(compile-html test env)])) 201 | (partition-all 2 cases))))) 202 | 203 | (defmethod compile-form "do" 204 | [[_ & forms] env] 205 | `(do ~@(butlast forms) ~(compile-html (last forms) env))) 206 | 207 | (defmethod compile-form "let" 208 | [[_ bindings & body] env] 209 | `(let ~bindings ~@(butlast body) ~(compile-html (last body) env))) 210 | 211 | (defmethod compile-form "let*" 212 | [[_ bindings & body] env] 213 | `(let* ~bindings ~@(butlast body) ~(compile-html (last body) env))) 214 | 215 | (defmethod compile-form "letfn*" 216 | [[_ bindings & body] env] 217 | `(letfn* ~bindings ~@(butlast body) ~(compile-html (last body) env))) 218 | 219 | (defmethod compile-form "for" 220 | [[_ bindings body] env] 221 | `(~'into-array (for ~bindings ~(compile-html body env)))) 222 | 223 | (defmethod compile-form "if" 224 | [[_ condition & body] env] 225 | `(if ~condition ~@(for [x body] (compile-html x env)))) 226 | 227 | (defmethod compile-form "if-not" 228 | [[_ bindings & body] env] 229 | `(if-not ~bindings ~@(doall (for [x body] (compile-html x env))))) 230 | 231 | (defmethod compile-form "if-some" 232 | [[_ bindings & body] env] 233 | `(if-some ~bindings ~@(doall (for [x body] (compile-html x env))))) 234 | 235 | (defmethod compile-form "when" 236 | [[_ bindings & body] env] 237 | `(when ~bindings ~@(doall (for [x body] (compile-html x env))))) 238 | 239 | (defmethod compile-form "when-not" 240 | [[_ bindings & body] env] 241 | `(when-not ~bindings ~@(doall (for [x body] (compile-html x env))))) 242 | 243 | (defmethod compile-form "when-some" 244 | [[_ bindings & body] env] 245 | `(when-some ~bindings ~@(butlast body) ~(compile-html (last body) env))) 246 | 247 | (defmethod compile-form :default 248 | [expr env] 249 | (if (:inline (meta expr)) 250 | expr `(interpret-maybe ~expr))) 251 | 252 | (defn- not-hint? 253 | "True if x is not hinted to be the supplied type." 254 | [x type] 255 | (if-let [hint (-> x meta :tag)] 256 | (not (isa? (eval hint) type)))) 257 | 258 | (defn- hint? 259 | "True if x is hinted to be the supplied type." 260 | [x type] 261 | (if-let [hint (-> x meta :tag)] 262 | (isa? (eval hint) type))) 263 | 264 | (defn- literal? 265 | "True if x is a literal value that can be rendered as-is." 266 | [x] 267 | (and (not (unevaluated? x)) 268 | (or (not (or (vector? x) (map? x))) 269 | (every? literal? x)))) 270 | 271 | (defn- not-implicit-map? 272 | "True if we can infer that x is not a map." 273 | [x] 274 | (or (= (form-name x) "for") 275 | (not (unevaluated? x)) 276 | (not-hint? x java.util.Map))) 277 | 278 | (defn- attrs-hint? 279 | "True if x has :attrs metadata. Treat x as a implicit map" 280 | [x] 281 | (-> x meta :attrs)) 282 | 283 | (defn- inline-hint? 284 | "True if x has :inline metadata. Treat x as a implicit map" 285 | [x] 286 | (-> x meta :inline)) 287 | 288 | (defn- element-compile-strategy 289 | "Returns the compilation strategy to use for a given element." 290 | [[tag attrs & content :as element] env] 291 | (cond 292 | ;; e.g. [:> ...] 293 | (= tag :>) 294 | ::react-interop 295 | 296 | ;; e.g. [:span "foo"] 297 | (every? literal? element) 298 | ::all-literal 299 | 300 | ;; e.g. [:span {} x] 301 | (and (literal? tag) (map? attrs)) 302 | ::literal-tag-and-attributes 303 | 304 | ;; e.g. [:span ^String x] 305 | (and (literal? tag) (not-implicit-map? attrs)) 306 | ::literal-tag-and-no-attributes 307 | 308 | ;; e.g. [:span ^:attrs y] or [:span (attrs)], return type of `attrs` is a map 309 | (and (literal? tag) 310 | (or (= '#{cljs.core/IMap} (infer-tag env attrs)) 311 | (attrs-hint? attrs))) 312 | ::literal-tag-and-hinted-attributes 313 | 314 | ;; e.g. [:span ^:inline (y)] 315 | (and (literal? tag) 316 | (or (primitive-type? (infer-tag env attrs)) 317 | (inline-hint? attrs))) 318 | ::literal-tag-and-inline-content 319 | 320 | ;; ; e.g. [:span x] 321 | (literal? tag) 322 | ::literal-tag 323 | 324 | ;; e.g. [x] 325 | :else 326 | ::default)) 327 | 328 | (declare compile-html) 329 | 330 | (defmulti compile-element 331 | "Returns an unevaluated form that will render the supplied vector as a HTML 332 | element." 333 | {:private true} 334 | element-compile-strategy) 335 | 336 | (defmethod compile-element ::react-interop 337 | [element env] 338 | `(rum.core/adapt-class ~@(rest element))) 339 | 340 | (defmethod compile-element ::all-literal 341 | [element env] 342 | (compile-react-element (eval element) env)) 343 | 344 | (defmethod compile-element ::literal-tag-and-attributes 345 | [[tag attrs & content] env] 346 | (let [[tag attrs _] (normalize/element [tag attrs])] 347 | `(daiquiri.core/create-element 348 | ~(compile-tag tag) 349 | ~(compile-attrs attrs) 350 | (cljs.core/array ~@(map #(compile-html % env) content))))) 351 | 352 | (defmethod compile-element ::literal-tag-and-no-attributes 353 | [[tag & content] env] 354 | (compile-element (apply vector tag {} content) env)) 355 | 356 | (defmethod compile-element ::literal-tag-and-inline-content 357 | [[tag & content] env] 358 | (compile-element (apply vector tag {} content) env)) 359 | 360 | (defmethod compile-element ::literal-tag-and-hinted-attributes 361 | [[tag attrs & content] env] 362 | (let [[tag tag-attrs _] (normalize/element [tag]) 363 | attrs-sym (gensym "attrs")] 364 | `(let [~attrs-sym ~attrs] 365 | (daiquiri.core/create-element 366 | ~(compile-tag tag) 367 | ~(compile-merge-attrs tag-attrs attrs-sym) 368 | ~(when-not (empty? content) 369 | `(cljs.core/array ~@(mapv #(compile-html % env) content))))))) 370 | 371 | (defmethod compile-element ::literal-tag 372 | [[tag attrs & content] env] 373 | (let [[tag tag-attrs _] (normalize/element [tag]) 374 | attrs-sym (gensym "attrs")] 375 | `(let [~attrs-sym ~attrs] 376 | (daiquiri.core/create-element 377 | ~(compile-tag tag) 378 | (if (map? ~attrs-sym) 379 | ~(compile-merge-attrs tag-attrs attrs-sym) 380 | ~(compile-attrs tag-attrs)) 381 | (if (map? ~attrs-sym) 382 | ~(when-not (empty? content) 383 | `(cljs.core/array ~@(mapv #(compile-html % env) content))) 384 | ~(when attrs 385 | `(cljs.core/array ~@(mapv #(compile-html % env) (cons attrs-sym content))))))))) 386 | 387 | (defmethod compile-element :default 388 | [element env] 389 | (maybe-warn-on-interpret env element) 390 | `(daiquiri.interpreter/interpret 391 | [~(first element) 392 | ~@(for [x (rest element)] 393 | (if (vector? x) 394 | (compile-element x env) 395 | x))])) 396 | 397 | (defn compile-html 398 | "Pre-compile data structures into HTML where possible." 399 | ([content] 400 | (compile-html content nil)) 401 | ([content env] 402 | (cond 403 | (vector? content) (compile-element content env) 404 | (literal? content) content 405 | (hint? content String) content 406 | (hint? content Number) content 407 | :else (compile-form content env)))) 408 | 409 | (defn compile-react [v env] 410 | (cond 411 | (vector? v) (if (element? v) 412 | (compile-react-element v env) 413 | (compile-react (seq v) env)) 414 | (seq? v) (map #(compile-react % env) v) 415 | :else v)) 416 | 417 | (defn- js-obj [m] 418 | (let [key-strs (mapv to-js (keys m)) 419 | kvs-str (->> (mapv #(-> (str \' % "':~{}")) key-strs) 420 | (interpose ",") 421 | (apply str))] 422 | (vary-meta 423 | (list* 'js* (str "{" kvs-str "}") (mapv to-js (vals m))) 424 | assoc :tag 'object))) 425 | 426 | (defn- to-js-map 427 | "Convert a map into a JavaScript object." 428 | [m] 429 | (if (every? literal? (keys m)) 430 | (js-obj m) 431 | `(daiquiri.interpreter/element-attributes ~m))) 432 | 433 | (defn- to-js-array 434 | "Convert a vector into a JavaScript array." 435 | [x] 436 | (apply list 'cljs.core/array (mapv to-js x))) 437 | 438 | (defn to-js [x] 439 | (cond 440 | (keyword? x) (name x) 441 | (map? x) (to-js-map x) 442 | (vector? x) (to-js-array x) 443 | :else x)) 444 | -------------------------------------------------------------------------------- /src/daiquiri/core.clj: -------------------------------------------------------------------------------- 1 | (ns daiquiri.core 2 | (:require [daiquiri.compiler :as compiler])) 3 | 4 | (defmacro attrs 5 | "Compile `attributes` map into a JavaScript literal." 6 | [attributes] 7 | (compiler/compile-attrs attributes)) 8 | 9 | (defmacro html 10 | "Compile the Hiccup `form`. Always produces code that evaluates to 11 | React elements." 12 | [form] 13 | (compiler/compile-html form &env)) 14 | -------------------------------------------------------------------------------- /src/daiquiri/core.cljs: -------------------------------------------------------------------------------- 1 | (ns daiquiri.core 2 | (:require [daiquiri.interpreter] 3 | [cljsjs.react])) 4 | 5 | (defn ^js/React.Element create-element 6 | "The React.js create element function." 7 | [type attrs children] 8 | (if ^boolean children 9 | (.apply (.-createElement js/React) nil (.concat #js [type attrs] children)) 10 | (.createElement js/React type attrs))) 11 | 12 | (def ^js/React.Fragment fragment 13 | "The React.js Fragment." 14 | (.-Fragment js/React)) 15 | -------------------------------------------------------------------------------- /src/daiquiri/interpreter.cljs: -------------------------------------------------------------------------------- 1 | (ns daiquiri.interpreter 2 | (:require [clojure.string :as str] 3 | [daiquiri.normalize :as normalize] 4 | [daiquiri.util :as util] 5 | [cljsjs.react] 6 | [goog.object :as gobj])) 7 | 8 | (defn ^js/React.Element create-element 9 | "Create a React element. Returns a JavaScript object when running 10 | under ClojureScript, and a om.dom.Element record in Clojure." 11 | [type attrs children] 12 | (.apply (.-createElement js/React) nil (.concat #js [type attrs] children))) 13 | 14 | (defn component-attributes [attrs] 15 | (let [x (util/camel-case-keys* attrs)] 16 | (let [m (js-obj)] 17 | (doseq [[k v] x] 18 | (gobj/set m (name k) v)) 19 | m))) 20 | 21 | (defn element-attributes [attrs] 22 | (when-let [js-attrs (clj->js (util/html-to-dom-attrs attrs))] 23 | (let [class (.-className js-attrs) 24 | class (if (array? class) (str/join " " class) class)] 25 | (when (.-onChange js-attrs) 26 | ;; Wrapping on-change handler to work around async rendering queue 27 | ;; that causes jumping caret and lost characters in input fields 28 | (set! (.-onChange js-attrs) (js/rum.core.mark_sync_update (.-onChange js-attrs)))) 29 | (if (str/blank? class) 30 | (js-delete js-attrs "className") 31 | (set! (.-className js-attrs) class)) 32 | js-attrs))) 33 | 34 | (declare interpret) 35 | 36 | (defn- ^array interpret-seq 37 | "Eagerly interpret the seq `x` as HTML elements." 38 | [x] 39 | (reduce 40 | (fn [^array ret x] 41 | (.push ret (interpret x)) 42 | ret) 43 | #js [] 44 | x)) 45 | 46 | (defn element 47 | "Render an element vector as a HTML element." 48 | [element] 49 | (let [[type attrs content] (normalize/element element)] 50 | (create-element type 51 | (element-attributes attrs) 52 | (interpret-seq content)))) 53 | 54 | (defn fragment [[_ attrs & children]] 55 | (let [[attrs children] (if (map? attrs) 56 | [(component-attributes attrs) (interpret-seq children)] 57 | [nil (interpret-seq (into [attrs] children))])] 58 | (create-element js/React.Fragment attrs children))) 59 | 60 | (defn interop [[_ component attrs & children]] 61 | (let [[attrs children] (if (map? attrs) 62 | [(component-attributes attrs) (interpret-seq children)] 63 | [nil (interpret-seq (into [attrs] children))])] 64 | (create-element component attrs children))) 65 | 66 | (defn- interpret-vec 67 | "Interpret the vector `x` as an HTML element or a the children of an 68 | element." 69 | [x] 70 | (cond 71 | (util/fragment? x) (fragment x) 72 | (keyword-identical? :> (nth x 0 nil)) (interop x) 73 | (util/element? x) (element x) 74 | :else (interpret-seq x))) 75 | 76 | (defn interpret [v] 77 | (cond 78 | (vector? v) (interpret-vec v) 79 | (seq? v) (interpret-seq v) 80 | :else v)) 81 | -------------------------------------------------------------------------------- /src/daiquiri/normalize.cljc: -------------------------------------------------------------------------------- 1 | (ns daiquiri.normalize 2 | (:require [clojure.string :as str] 3 | [daiquiri.util :as util])) 4 | 5 | (defn class-name 6 | [x] 7 | (cond 8 | (string? x) x 9 | (keyword? x) (name x) 10 | :else x)) 11 | 12 | (defn map-lookup? 13 | "Returns true if `x` is a map lookup form, otherwise false." 14 | [x] 15 | (and (list? x) (keyword? (first x)))) 16 | 17 | (defn normalize-class 18 | "Normalize `class` into a vector of classes." 19 | [class] 20 | (cond 21 | (nil? class) 22 | nil 23 | 24 | (map-lookup? class) 25 | [class] 26 | 27 | (list? class) 28 | (if (symbol? (first class)) 29 | [class] 30 | (map class-name class)) 31 | 32 | (symbol? class) 33 | [class] 34 | 35 | (string? class) 36 | [class] 37 | 38 | (keyword? class) 39 | [(class-name class)] 40 | 41 | (and (or (set? class) 42 | (sequential? class)) 43 | (every? #(or (keyword? %) 44 | (string? %)) 45 | class)) 46 | (mapv class-name class) 47 | 48 | (and (or (set? class) 49 | (sequential? class))) 50 | (mapv class-name class) 51 | 52 | :else class)) 53 | 54 | (defn attributes 55 | "Normalize the `attrs` of an element." 56 | [attrs] 57 | (cond-> attrs 58 | (:class attrs) 59 | (update :class normalize-class))) 60 | 61 | (defn merge-with-class 62 | "Like clojure.core/merge but concatenate :class entries." 63 | [& maps] 64 | (let [maps (map attributes maps) 65 | classes (mapcat :class maps)] 66 | (when (seq maps) 67 | (cond-> (reduce into {} maps) 68 | (not (empty? classes)) 69 | (assoc :class (vec classes)))))) 70 | 71 | (defn strip-css 72 | "Strip the # and . characters from the beginning of `s`." 73 | [s] 74 | (when s 75 | (str/replace s #"^[.#]" ""))) 76 | 77 | (defn match-tag 78 | "Match `s` as a CSS tag and return a vector of tag name, CSS id and 79 | CSS classes." 80 | [s] 81 | (let [matches (re-seq #"[#.]?[^#.]+" (name s)) 82 | [tag-name names] 83 | (cond (empty? matches) 84 | (throw (ex-info (str "Can't match CSS tag: " s) {:tag s})) 85 | 86 | (contains? #{\# \.} (ffirst matches)) ;; shorthand for div 87 | ["div" matches] 88 | 89 | :default 90 | [(first matches) (rest matches)])] 91 | [tag-name 92 | (strip-css (some #(when (= \# (first %1)) %1) names)) 93 | (into [] 94 | (keep #(when (= \. (first %)) (strip-css %))) 95 | names)])) 96 | 97 | (defn children 98 | "Normalize the children of a HTML element." 99 | [x] 100 | (->> (cond 101 | (nil? x) 102 | '() 103 | (string? x) 104 | (list x) 105 | 106 | (util/element? x) 107 | (list x) 108 | (and (list? x) (symbol? (first x))) 109 | (list x) 110 | 111 | (list? x) 112 | x 113 | 114 | (and (sequential? x) 115 | (= (count x) 1) 116 | (sequential? (first x)) 117 | (not (string? (first x))) 118 | (not (util/element? (first x)))) 119 | (children (first x)) 120 | 121 | (sequential? x) 122 | x 123 | 124 | :else (list x)) 125 | (remove nil?))) 126 | 127 | (defn- attrs? 128 | "Returns true if `x` are the attributes of an HTML element, 129 | otherwise false." 130 | [x] 131 | (map? x)) 132 | 133 | (defn element 134 | "Ensure an element vector is of the form [tag-name attrs content]." 135 | [[tag & content]] 136 | (when-not (or (keyword? tag) 137 | (symbol? tag) 138 | (string? tag)) 139 | (throw (ex-info (str tag " is not a valid element name.") {:tag tag :content content}))) 140 | (let [[tag id class] (match-tag tag) 141 | tag-attrs (cond-> {} 142 | (not (empty? id)) (assoc :id id) 143 | (not (empty? class)) (assoc :class class)) 144 | map-attrs (first content)] 145 | (if (attrs? map-attrs) 146 | [tag 147 | (merge-with-class tag-attrs map-attrs) 148 | (children (next content))] 149 | [tag 150 | (attributes tag-attrs) 151 | (children content)]))) 152 | -------------------------------------------------------------------------------- /src/daiquiri/util.cljc: -------------------------------------------------------------------------------- 1 | (ns daiquiri.util 2 | (:require [clojure.set :refer [rename-keys]] 3 | [clojure.string :as str])) 4 | 5 | (defn valid-key? [k] 6 | (or (keyword? k) 7 | (string? k) 8 | (symbol? k))) 9 | 10 | (defn -camel-case [k] 11 | (if (string? k) 12 | k 13 | (let [[first-word & words] (.split (name k) "-")] 14 | (if (or (empty? words) 15 | (= "aria" first-word) 16 | (= "data" first-word)) 17 | k 18 | (-> (map str/capitalize words) 19 | (conj first-word) 20 | str/join 21 | keyword))))) 22 | 23 | (def attrs-cache (volatile! {})) 24 | 25 | (defn camel-case 26 | "Returns camel case version of the key, e.g. :http-equiv becomes :httpEquiv. 27 | Does not convert string attributes." 28 | [k] 29 | (if (valid-key? k) 30 | (or (get @attrs-cache k) 31 | (let [kk (-camel-case k)] 32 | (vswap! attrs-cache assoc k kk) 33 | kk)) 34 | k)) 35 | 36 | (defn camel-case-keys* [m] 37 | (->> (reduce-kv #(assoc! %1 (camel-case %2) %3) 38 | (transient {}) 39 | m) 40 | persistent!)) 41 | 42 | (defn camel-case-keys 43 | "Recursively transforms all map keys into camel case." 44 | [m] 45 | (if (map? m) 46 | (let [m (->> m 47 | (reduce-kv #(assoc! %1 (camel-case %2) %3) 48 | (transient {})) 49 | persistent!)] 50 | (cond-> m 51 | (map? (:style m)) 52 | (update :style camel-case-keys))) 53 | m)) 54 | 55 | (defn fragment-tag? 56 | "Returns true if `tag` is the fragment tag \"*\" or \"<>\", otherwise false." 57 | [tag] 58 | (and (or (keyword? tag) 59 | (symbol? tag) 60 | (string? tag)) 61 | (or (= (name tag) "*") 62 | (= (name tag) "<>")))) 63 | 64 | (defn fragment? [v] 65 | (and (vector? v) 66 | (fragment-tag? (nth v 0 nil)))) 67 | 68 | (defn element? 69 | "Return true if `x` is an HTML element. True when `x` is a vector 70 | and the first element is a keyword, e.g. `[:div]` or `[:div [:span \"x\"]`." 71 | [x] 72 | (and (vector? x) 73 | (keyword? (nth x 0 nil)))) 74 | 75 | (defn html-to-dom-attrs 76 | "Converts all HTML attributes to their DOM equivalents." 77 | [attrs] 78 | (rename-keys (camel-case-keys attrs) 79 | {:class :className 80 | :for :htmlFor})) 81 | 82 | (defn join-classes 83 | "Join the `classes` with a whitespace." 84 | [classes] 85 | (->> classes 86 | (into [] (comp 87 | (mapcat (fn [x] (if (string? x) [x] (seq x)))) 88 | (remove nil?))) 89 | (str/join " "))) 90 | -------------------------------------------------------------------------------- /src/rum/core.clj: -------------------------------------------------------------------------------- 1 | (ns rum.core 2 | (:refer-clojure :exclude [ref deref]) 3 | (:require 4 | [rum.cursor :as cursor] 5 | [rum.server-render :as render] 6 | [rum.util :refer [collect collect* call-all]] 7 | [rum.derived-atom :as derived-atom] 8 | [daiquiri.compiler :as compiler] 9 | [rum.specs] 10 | [clojure.set :as set]) 11 | (:import 12 | [rum.cursor Cursor] 13 | (rum.server_render JSComponent))) 14 | 15 | (defmacro set-warn-on-interpretation! [v] 16 | (reset! compiler/warn-on-interpretation v)) 17 | 18 | (defn- fn-body? [form] 19 | (when (and (seq? form) 20 | (vector? (first form))) 21 | (if (= '< (second form)) 22 | (throw (IllegalArgumentException. "Mixins must be given before argument list")) 23 | true))) 24 | 25 | (defn- parse-defc 26 | ":name :doc? env :ns :name (str "/" name)) 61 | (str name)) 62 | var-sym (-> name 63 | (vary-meta update :arglists #(or (:arglists %) `(quote ~arglists))) 64 | (vary-meta assoc :rum/tag `'js/React.Element))] 65 | `(def ~var-sym 66 | ~@(if doc [doc] []) 67 | ~(if cljs? 68 | `(rum.core/lazy-build ~builder (fn ~@render-bodies) ~mixins ~display-name) 69 | `(~builder (fn ~@render-bodies) ~mixins ~display-name))))) 70 | 71 | (defmacro defc 72 | "``` 73 | (defc name doc-string? (< mixins+)? [ params* ] render-body+) 74 | ``` 75 | 76 | Defc does couple of things: 77 | 78 | 1. Wraps body into daiquiri/compile-html 79 | 2. Generates render function from that 80 | 3. Takes render function and mixins, builds React class from them 81 | 4. Using that class, generates constructor fn [args]->ReactElement 82 | 5. Defines top-level var with provided name and assigns ctor to it 83 | 84 | Usage: 85 | 86 | ``` 87 | (rum/defc label < rum/static [t] 88 | [:div t]) 89 | 90 | ;; creates React class 91 | ;; adds mixin rum/static 92 | ;; defines ctor fn (defn label [t] ...) => element 93 | 94 | (label \"text\") ;; => returns React element built with label class 95 | ```" 96 | [& body] 97 | (-defc 'rum.core/build-defc &env body)) 98 | 99 | (defmacro defcs 100 | "``` 101 | (defcs name doc-string? (< mixins+)? [ state-arg params* ] render-body+) 102 | ``` 103 | 104 | Same as [[defc]], but render will take additional first argument: component state." 105 | [& body] 106 | (-defc 'rum.core/build-defcs &env body)) 107 | 108 | (defmacro defcc 109 | "``` 110 | (defcc name doc-string? (< mixins+)? [ comp-arg params* ] render-body+) 111 | ``` 112 | 113 | Same as [[defc]], but render will take additional first argument: react component." 114 | [& body] 115 | (-defc 'rum.core/build-defcc &env body)) 116 | 117 | (defn- build-ctor [render mixins display-name] 118 | (let [mixins (->> mixins (mapcat keys) set)] 119 | (assert (set/subset? mixins rum.specs/mixins) 120 | (str display-name " declares invalid mixin keys " 121 | (set/difference mixins rum.specs/mixins) ", " 122 | "did you mean one of " rum.specs/mixins))) 123 | (let [init (collect :init mixins) ;; state props -> state 124 | will-mount (collect* [:will-mount ;; state -> state 125 | :before-render] mixins) ;; state -> state 126 | did-catch (collect :did-catch mixins) ;; state error info -> state 127 | render render ;; state -> [dom state] 128 | wrapped-render (reduce #(%2 %1) render (collect :wrap-render mixins))] ;; render-fn -> render-fn 129 | (fn [& args] 130 | (let [props nil 131 | state (-> {:rum/args args} 132 | (call-all init props) 133 | (call-all will-mount)) 134 | [dom _] (if (empty? did-catch) 135 | (wrapped-render state) 136 | (try 137 | (wrapped-render state) 138 | (catch Exception e 139 | (wrapped-render (call-all state did-catch e nil)))))] 140 | (or dom [:rum/nothing]))))) 141 | 142 | (defn ^:no-doc build-defc [render-body mixins display-name] 143 | (if (empty? mixins) 144 | (fn [& args] (or (apply render-body args) [:rum/nothing])) 145 | (let [render (fn [state] [(apply render-body (:rum/args state)) state])] 146 | (build-ctor render mixins display-name)))) 147 | 148 | (defn ^:no-doc build-defcs [render-body mixins display-name] 149 | (let [render (fn [state] [(apply render-body state (:rum/args state)) state])] 150 | (build-ctor render mixins display-name))) 151 | 152 | (defn ^:no-doc build-defcc [render-body mixins display-name] 153 | (let [render (fn [state] [(apply render-body (:rum/react-component state) (:rum/args state)) state])] 154 | (build-ctor render mixins display-name))) 155 | 156 | 157 | ;; rum.core APIs 158 | 159 | 160 | (defn with-key 161 | "Adds React key to element. 162 | 163 | ``` 164 | (rum/defc label [text] [:div text]) 165 | 166 | (-> (label) 167 | (rum/with-key \"abc\") 168 | (rum/mount js/document.body)) 169 | ```" 170 | [element key] 171 | ;; Roman. Why we are doing this for SSR? Keys are not used on the server 172 | (cond 173 | (render/nothing? element) 174 | element 175 | 176 | (and (vector? element) (map? (get element 1))) 177 | (assoc-in element [1 :key] key) 178 | 179 | (vector? element) 180 | (into [(first element) {:key key}] (next element)) 181 | 182 | :else element)) 183 | 184 | (defn with-ref 185 | "Supported, does nothing." 186 | [element ref] 187 | element) 188 | 189 | 190 | ;; mixins 191 | 192 | 193 | (def static "Supported, does nothing." {}) 194 | 195 | (defn local 196 | "Mixin constructor. Adds an atom to component’s state that can be used to keep stuff during component’s lifecycle. Component will be re-rendered if atom’s value changes. Atom is stored under user-provided key or under `:rum/local` by default. 197 | 198 | ``` 199 | (rum/defcs counter < (rum/local 0 :cnt) 200 | [state label] 201 | (let [*cnt (:cnt state)] 202 | [:div {:on-click (fn [_] (swap! *cnt inc))} 203 | label @*cnt])) 204 | 205 | (rum/mount (counter \"Click count: \")) 206 | ```" 207 | ([initial] (local initial :rum/local)) 208 | ([initial key] 209 | {:will-mount (fn [state] 210 | (assoc state key (atom initial)))})) 211 | 212 | (def reactive "Supported, does nothing." {}) 213 | 214 | (def ^{:arglists '([ref]) 215 | :doc "Supported as simple deref."} 216 | react clojure.core/deref) 217 | 218 | (defn cursor-in 219 | "Given atom with deep nested value and path inside it, creates an atom-like structure 220 | that can be used separately from main atom, but will sync changes both ways: 221 | 222 | ``` 223 | (def db (atom { :users { \"Ivan\" { :age 30 }}})) 224 | 225 | (def ivan (rum/cursor db [:users \"Ivan\"])) 226 | (deref ivan) ;; => { :age 30 } 227 | 228 | (swap! ivan update :age inc) ;; => { :age 31 } 229 | (deref db) ;; => { :users { \"Ivan\" { :age 31 }}} 230 | 231 | (swap! db update-in [:users \"Ivan\" :age] inc) 232 | ;; => { :users { \"Ivan\" { :age 32 }}} 233 | 234 | (deref ivan) ;; => { :age 32 } 235 | ``` 236 | 237 | Returned value supports `deref`, `swap!`, `reset!`, watches and metadata. 238 | 239 | The only supported option is `:meta`" 240 | ^rum.cursor.Cursor [ref path & {:as options}] 241 | (if (instance? Cursor ref) 242 | (cursor/Cursor. (.-ref ^Cursor ref) (into (.-path ^Cursor ref) path) (:meta options) (volatile! {})) 243 | (cursor/Cursor. ref path (:meta options) (volatile! {})))) 244 | 245 | (defn cursor 246 | "Same as [[cursor-in]] but accepts single key instead of path vector." 247 | ^rum.cursor.Cursor [ref key & options] 248 | (apply cursor-in ref [key] options)) 249 | 250 | (def ^{:style/indent 2 251 | :arglists '([refs key f] [refs key f opts]) 252 | :doc "Use this to create “chains” and acyclic graphs of dependent atoms. 253 | 254 | [[derived-atom]] will: 255 | 256 | - Take N “source” refs. 257 | - Set up a watch on each of them. 258 | - Create “sink” atom. 259 | - When any of source refs changes: 260 | - re-run function `f`, passing N dereferenced values of source refs. 261 | - `reset!` result of `f` to the sink atom. 262 | - Return sink atom. 263 | 264 | Example: 265 | 266 | ``` 267 | (def *a (atom 0)) 268 | (def *b (atom 1)) 269 | (def *x (derived-atom [*a *b] ::key 270 | (fn [a b] 271 | (str a \":\" b)))) 272 | 273 | (type *x) ;; => clojure.lang.Atom 274 | (deref *x) ;; => \"0:1\" 275 | 276 | (swap! *a inc) 277 | (deref *x) ;; => \"1:1\" 278 | 279 | (reset! *b 7) 280 | (deref *x) ;; => \"1:7\" 281 | ``` 282 | 283 | Arguments: 284 | 285 | - `refs` - sequence of source refs, 286 | - `key` - unique key to register watcher, same as in `clojure.core/add-watch`, 287 | - `f` - function that must accept N arguments (same as number of source refs) and return a value to be written to the sink ref. Note: `f` will be called with already dereferenced values, 288 | - `opts` - optional. Map of: 289 | - `:ref` - use this as sink ref. By default creates new atom, 290 | - `:check-equals?` - Defaults to `true`. If equality check should be run on each source update: `(= @sink (f new-vals))`. When result of recalculating `f` equals to the old value, `reset!` won’t be called. Set to `false` if checking for equality can be expensive."} 291 | derived-atom derived-atom/derived-atom) 292 | 293 | 294 | ;;; Server-side rendering 295 | 296 | 297 | (def ^{:arglists '([element] [element opts]) 298 | :doc "Main server-side rendering method. Given component, returns HTML string with static markup of that component. Serve that string to the browser and [[hydrate]] same Rum component over it. React will be able to reuse already existing DOM and will initialize much faster. No opts are supported at the moment."} 299 | render-html render/render-html) 300 | 301 | (def ^{:arglists '([element]) 302 | :doc "Same as [[render-html]] but returned string has nothing React-specific. This allows Rum to be used as traditional server-side templating engine."} 303 | render-static-markup render/render-static-markup) 304 | 305 | 306 | ;; method parity with CLJS version so you can avoid conditional directive 307 | ;; in e.g. did-mount/will-unmount mixin bodies 308 | 309 | 310 | (defn ^:no-doc state [c] 311 | (throw (UnsupportedOperationException. "state is only available from ClojureScript"))) 312 | 313 | (defn ^:no-doc dom-node [s] 314 | (throw (UnsupportedOperationException. "dom-node is only available from ClojureScript"))) 315 | 316 | (defn ^:no-doc ref [s k] 317 | (throw (UnsupportedOperationException. "ref is only available from ClojureScript"))) 318 | 319 | (defn ^:no-doc ref-node [s k] 320 | (throw (UnsupportedOperationException. "ref is only available from ClojureScript"))) 321 | 322 | (defn ^:no-doc mount [c n] 323 | (throw (UnsupportedOperationException. "mount is only available from ClojureScript"))) 324 | 325 | (defn ^:no-doc unmount [c] 326 | (throw (UnsupportedOperationException. "unmount is only available from ClojureScript"))) 327 | 328 | (defn ^:no-doc request-render [c] 329 | (throw (UnsupportedOperationException. "request-render is only available from ClojureScript"))) 330 | 331 | ;; Context API 332 | 333 | (defn- sym->context-name [name env] 334 | (str "Context(" (-> env :ns :name (str "/" name)) ")")) 335 | 336 | (defmacro defcontext 337 | "cljs: Creates React context with initial value set to `value`. 338 | clj: Create dynamic var bound to `value`." 339 | ([name] 340 | (if (:ns &env) 341 | `(def ~(with-meta name {:dynamic true}) (let [ctx# (create-context nil)] 342 | (set! (.-displayName ctx#) ~(sym->context-name name &env)) 343 | ctx#)) 344 | `(def ~(with-meta name {:dynamic true})))) 345 | ([name value] 346 | (if (:ns &env) 347 | `(def ~(with-meta name {:dynamic true}) (let [ctx# (create-context ~value)] 348 | (set! (.-displayName ctx#) ~(sym->context-name name &env)) 349 | ctx#)) 350 | `(def ~(with-meta name {:dynamic true}) ~value)))) 351 | 352 | (defmacro with-context 353 | "(with-context [value ctx] 354 | [:div value])" 355 | [[sym context] & body] 356 | (if (:ns &env) 357 | `(.createElement js/React (.-Consumer ~context) nil (fn [~sym] ~@(map #(compiler/compile-html % &env) body))) 358 | `(let [~sym ~context] 359 | ~@body))) 360 | 361 | (defmacro bind-context 362 | "(bind-context [context value] 363 | ...)" 364 | [[context value] & body] 365 | (if (:ns &env) 366 | (let [ctx (with-meta (gensym "ctx") {:tag 'js})] 367 | `(let [~ctx ~context] 368 | (.createElement js/React (.-Provider ~ctx) 369 | (cljs.core/js-obj "value" ~value) 370 | ~@(map #(compiler/compile-html % &env) body)))) 371 | `(binding [~context ~value] 372 | ~@body))) 373 | 374 | ;; hooks 375 | 376 | (defn use-state [value-or-fn] 377 | (if (fn? value-or-fn) 378 | [(value-or-fn) identity] 379 | [value-or-fn identity])) 380 | 381 | (defn use-reducer [reducer-fn initial-value] 382 | [initial-value reducer-fn]) 383 | 384 | (defn use-effect! 385 | ([setup-fn]) 386 | ([setup-fn deps])) 387 | 388 | (defn use-layout-effect! 389 | ([setup-fn]) 390 | ([setup-fn deps])) 391 | 392 | (defn use-callback 393 | ([callback] callback) 394 | ([callback deps] callback)) 395 | 396 | (defn use-memo 397 | ([f] (f)) 398 | ([f deps] (f))) 399 | 400 | (defn use-ref [initial-value] 401 | (atom initial-value)) 402 | 403 | ;; Refs 404 | 405 | (defn create-ref [] 406 | (atom nil)) 407 | 408 | (defn deref [ref] 409 | @ref) 410 | 411 | (defn set-ref! [ref value] 412 | (reset! ref value)) 413 | 414 | ;; React.Suspense 415 | 416 | (defmacro suspense 417 | "(rum/require-lazy '[app.components :refer [alert]]) 418 | 419 | (rum/defc root [] 420 | (suspense {:fallback \"Hello!\"} 421 | (alert \"ARGUMENT\"))) 422 | 423 | See a complete example here https://github.com/roman01la/rum-code-splitting" 424 | [{:keys [fallback]} child] 425 | (if-not (:ns &env) 426 | child 427 | `(.createElement js/React 428 | (.-Suspense js/React) 429 | (cljs.core/js-obj "fallback" ~fallback) 430 | ~(compiler/compile-html child &env)))) 431 | 432 | ;; React.Fragment 433 | 434 | (defmacro fragment 435 | "(rum/fragment [button] [input] ...)" 436 | [attrs & children] 437 | (let [[attrs children] (if (map? attrs) 438 | [attrs children] 439 | [nil (into [attrs] children)])] 440 | (if-not (:ns &env) 441 | `(list ~@children) 442 | `(.createElement js/React 443 | (.-Fragment js/React) 444 | ~(compiler/to-js attrs) 445 | ~@(map #(compiler/compile-html % &env) children))))) 446 | 447 | ;; JS components adapter 448 | (def ^{:arglists '([type-sym attrs children]) 449 | :dynamic true 450 | :doc "Takes JS component name as a symbol, attributes map and a collection of child elements. Should return a string."} 451 | *render-js-component*) 452 | 453 | (defmacro adapt-class 454 | "Adapts JavaScript React component for usage in Rum components. 455 | 456 | [:div 457 | (rum/adapt-class js/Button {:on-click f} \"press me\")] 458 | 459 | When using in Clojure JVM calls *render-js-component* to render a fallback. 460 | See example in rum.examples.js-components ns" 461 | [type attrs & children] 462 | (let [[attrs children] (if (map? attrs) 463 | [attrs children] 464 | [nil (cons attrs children)])] 465 | (if (:ns &env) 466 | `(adapt-class-helper ~type ~(compiler/to-js attrs) (cljs.core/array ~@(map #(compiler/compile-html % &env) children))) 467 | `(JSComponent. (*render-js-component* '~type ~attrs [~@children]))))) 468 | -------------------------------------------------------------------------------- /src/rum/cursor.clj: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc rum.cursor) 2 | 3 | (deftype Cursor [ref path ^:volatile-mutable meta watches] 4 | clojure.lang.IDeref 5 | 6 | (deref [this] 7 | (get-in (deref ref) path)) 8 | 9 | clojure.lang.IRef 10 | 11 | (setValidator [this vf] 12 | (throw (UnsupportedOperationException. "rum.cursor.Cursor/setValidator"))) 13 | 14 | (getValidator [this] 15 | (throw (UnsupportedOperationException. "rum.cursor.Cursor/getValidator"))) 16 | 17 | (getWatches [this] 18 | @watches) 19 | 20 | (addWatch [this key callback] 21 | (vswap! watches assoc key callback) 22 | (add-watch ref (list this key) 23 | (fn [_ _ oldv newv] 24 | (let [old (get-in oldv path) 25 | new (get-in newv path)] 26 | (when (not= old new) 27 | (callback key this old new))))) 28 | this) 29 | 30 | (removeWatch [this key] 31 | (vswap! watches dissoc key) 32 | (remove-watch ref (list this key)) 33 | this) 34 | 35 | clojure.lang.IAtom 36 | 37 | (swap [this f] 38 | (-> (swap! ref update-in path f) 39 | (get-in path))) 40 | 41 | (swap [this f a] 42 | (-> (swap! ref update-in path f a) 43 | (get-in path))) 44 | 45 | (swap [this f a b] 46 | (-> (swap! ref update-in path f a b) 47 | (get-in path))) 48 | 49 | (swap [this f a b rest] 50 | (-> (apply swap! ref update-in path f a b rest) 51 | (get-in path))) 52 | 53 | (compareAndSet [this oldv newv] 54 | (loop [] 55 | (let [refv @ref] 56 | (if (not= oldv (get-in refv path)) 57 | false 58 | (or (compare-and-set! ref refv (assoc-in refv path newv)) 59 | (recur)))))) 60 | 61 | (reset [this newv] 62 | (swap! ref assoc-in path newv) 63 | newv) 64 | 65 | clojure.lang.IMeta 66 | 67 | (meta [this] 68 | meta) 69 | 70 | clojure.lang.IReference 71 | 72 | (alterMeta [this f args] 73 | (.resetMeta this (apply f meta args))) 74 | 75 | (resetMeta [this m] 76 | (set! meta m) 77 | m)) 78 | -------------------------------------------------------------------------------- /src/rum/cursor.cljs: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc rum.cursor) 2 | 3 | (deftype Cursor [ref path meta] 4 | Object 5 | (equiv [this other] 6 | (-equiv this other)) 7 | 8 | IAtom 9 | 10 | IMeta 11 | (-meta [_] meta) 12 | 13 | IEquiv 14 | (-equiv [this other] 15 | (identical? this other)) 16 | 17 | IDeref 18 | (-deref [_] 19 | (get-in (-deref ref) path)) 20 | 21 | IWatchable 22 | (-add-watch [this key callback] 23 | (add-watch ref (list this key) 24 | (fn [_ _ oldv newv] 25 | (let [old (get-in oldv path) 26 | new (get-in newv path)] 27 | (when (not= old new) 28 | (callback key this old new))))) 29 | this) 30 | 31 | (-remove-watch [this key] 32 | (remove-watch ref (list this key)) 33 | this) 34 | 35 | IHash 36 | (-hash [this] (goog/getUid this)) 37 | 38 | IReset 39 | (-reset! [_ newv] 40 | (swap! ref assoc-in path newv) 41 | newv) 42 | 43 | ISwap 44 | (-swap! [this f] 45 | (-reset! this (f (-deref this)))) 46 | (-swap! [this f a] 47 | (-reset! this (f (-deref this) a))) 48 | (-swap! [this f a b] 49 | (-reset! this (f (-deref this) a b))) 50 | (-swap! [this f a b rest] 51 | (-reset! this (apply f (-deref this) a b rest))) 52 | 53 | IPrintWithWriter 54 | (-pr-writer [this writer opts] 55 | (-write writer "#object [rum.cursor.Cursor ") 56 | (pr-writer {:val (-deref this)} writer opts) 57 | (-write writer "]"))) 58 | 59 | -------------------------------------------------------------------------------- /src/rum/derived_atom.cljc: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc rum.derived-atom) 2 | 3 | (defn derived-atom 4 | ([refs key f] 5 | (derived-atom refs key f {})) 6 | ([refs key f opts] 7 | (let [{:keys [ref check-equals?] 8 | :or {check-equals? true}} opts 9 | recalc (case (count refs) 10 | 1 (let [[a] refs] #(f @a)) 11 | 2 (let [[a b] refs] #(f @a @b)) 12 | 3 (let [[a b c] refs] #(f @a @b @c)) 13 | #(apply f (map deref refs))) 14 | sink (if ref 15 | (doto ref (reset! (recalc))) 16 | (atom (recalc))) 17 | watch (if check-equals? 18 | (fn [_ _ _ _] 19 | (let [new-val (recalc)] 20 | (when (not= @sink new-val) 21 | (reset! sink new-val)))) 22 | (fn [_ _ _ _] 23 | (reset! sink (recalc))))] 24 | (doseq [ref refs] 25 | (add-watch ref key watch)) 26 | sink))) 27 | -------------------------------------------------------------------------------- /src/rum/lazy_loader.cljc: -------------------------------------------------------------------------------- 1 | (ns rum.lazy-loader 2 | #?(:cljs (:require-macros [rum.lazy-loader])) 3 | #?(:clj (:require [clojure.spec.alpha :as s] 4 | [cljs.core.specs.alpha]) 5 | :cljs (:require [cljs.loader] 6 | [cljsjs.react]))) 7 | 8 | #?(:clj 9 | (s/def :lazy/libspec 10 | (s/and 11 | seq? 12 | (s/cat 13 | :quote #{'quote} 14 | :libspec (s/spec 15 | (s/cat 16 | :lib simple-symbol? 17 | :marker #{:refer} 18 | :refer :cljs.core.specs.alpha/refer)))))) 19 | 20 | #?(:cljs 21 | (def react-lazy (.-lazy js/React))) 22 | 23 | #?(:cljs 24 | (def load! cljs.loader/load)) 25 | 26 | #?(:clj 27 | (s/fdef require-lazy 28 | :args (s/cat :form :lazy/libspec))) 29 | 30 | #?(:clj 31 | (defmacro require-lazy 32 | "require-like macro, returns lazy-loaded React components. 33 | (require-lazy '[my.ns.components :refer [c1 c2]])" 34 | [form] 35 | (if-not (:ns &env) 36 | `(clojure.core/require ~form) 37 | (let [m (s/conform :lazy/libspec form)] 38 | (when (not= m :clojure.spec.alpha/invalid) 39 | (let [{:keys [lib refer]} (:libspec m) 40 | module (->> (str lib) 41 | (re-find #"\.([a-z0-9-]+)") 42 | second 43 | keyword)] 44 | `(do 45 | ~@(for [sym refer] 46 | (let [qualified-sym (symbol (str lib "/" sym)) 47 | on-load `(fn [ok# fail#] 48 | (load! ~module (fn [] 49 | (ok# (cljs.core/js-obj "default" #(apply (deref (cljs.core/resolve '~qualified-sym)) (aget % ":rum/args")))))))] 50 | `(let [lazy# (react-lazy (fn [] (~'js/Promise. ~on-load)))] 51 | (defn ~sym [& args#] 52 | (.createElement js/React lazy# (cljs.core/js-obj ":rum/args" args#))))))))))))) 53 | -------------------------------------------------------------------------------- /src/rum/server_render.clj: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc rum.server-render 2 | (:require 3 | [clojure.string :as str]) 4 | (:import 5 | [clojure.lang IPersistentVector ISeq Ratio Keyword])) 6 | 7 | (defn nothing? [element] 8 | (and (vector? element) 9 | (= :rum/nothing (first element)))) 10 | 11 | (def ^:dynamic *select-value*) 12 | (def ^:dynamic *multiple?*) 13 | 14 | (defn append! 15 | ([^StringBuilder sb s0] (.append sb s0)) 16 | ([^StringBuilder sb s0 s1] 17 | (.append sb s0) 18 | (.append sb s1)) 19 | ([^StringBuilder sb s0 s1 s2] 20 | (.append sb s0) 21 | (.append sb s1) 22 | (.append sb s2)) 23 | ([^StringBuilder sb s0 s1 s2 s3] 24 | (.append sb s0) 25 | (.append sb s1) 26 | (.append sb s2) 27 | (.append sb s3)) 28 | ([^StringBuilder sb s0 s1 s2 s3 s4] 29 | (.append sb s0) 30 | (.append sb s1) 31 | (.append sb s2) 32 | (.append sb s3) 33 | (.append sb s4))) 34 | 35 | (defprotocol ToString 36 | (^String to-str [x] "Convert a value into a string.")) 37 | 38 | (extend-protocol ToString 39 | Keyword (to-str [k] (name k)) 40 | Ratio (to-str [r] (str (float r))) 41 | String (to-str [s] s) 42 | Object (to-str [x] (str x)) 43 | nil (to-str [_] "")) 44 | 45 | (def ^{:doc "A list of elements that must be rendered without a closing tag." 46 | :private true} 47 | void-tags 48 | #{"area" "base" "br" "col" "command" "embed" "hr" "img" "input" "keygen" "link" 49 | "meta" "param" "source" "track" "wbr"}) 50 | 51 | (def normalized-attrs 52 | {;; special cases 53 | :default-checked "checked" 54 | :default-value "value" 55 | 56 | ;; https://github.com/facebook/react/blob/v15.6.2/src/renderers/dom/shared/HTMLDOMPropertyConfig.js 57 | :accept-charset "accept-charset" 58 | :access-key "accessKey" 59 | :allow-full-screen "allowfullscreen" 60 | :allow-transparency "allowTransparency" 61 | :auto-complete "autoComplete" 62 | :auto-play "autoplay" 63 | :cell-padding "cellPadding" 64 | :cell-spacing "cellSpacing" 65 | :char-set "charSet" 66 | :class-id "classId" 67 | :col-span "colSpan" 68 | :content-editable "contenteditable" 69 | :context-menu "contextMenu" 70 | :cross-origin "crossorigin" 71 | :date-time "dateTime" 72 | :enc-type "encType" 73 | :form-action "formAction" 74 | :form-enc-type "formEncType" 75 | :form-method "formMethod" 76 | :form-no-validate "formnovalidate" 77 | :form-target "formTarget" 78 | :frame-border "frameBorder" 79 | :href-lang "hrefLang" 80 | :http-equiv "http-equiv" 81 | :input-mode "inputMode" 82 | :key-params "keyParams" 83 | :key-type "keyType" 84 | :margin-height "marginHeight" 85 | :margin-width "marginWidth" 86 | :max-length "maxLength" 87 | :media-group "mediaGroup" 88 | :min-length "minLength" 89 | :no-validate "novalidate" 90 | :radio-group "radioGroup" 91 | :referrer-policy "referrerPolicy" 92 | :read-only "readonly" 93 | :row-span "rowspan" 94 | :spell-check "spellcheck" 95 | :src-doc "srcDoc" 96 | :src-lang "srcLang" 97 | :src-set "srcSet" 98 | :tab-index "tabindex" 99 | :use-map "useMap" 100 | :auto-capitalize "autoCapitalize" 101 | :auto-correct "autoCorrect" 102 | :auto-save "autoSave" 103 | :item-prop "itemProp" 104 | :item-scope "itemscope" 105 | :item-type "itemType" 106 | :item-id "itemId" 107 | :item-ref "itemRef" 108 | 109 | ;; https://github.com/facebook/react/blob/v15.6.2/src/renderers/dom/shared/SVGDOMPropertyConfig.js 110 | :allow-reorder "allowReorder" 111 | :attribute-name "attributeName" 112 | :attribute-type "attributeType" 113 | :auto-reverse "autoReverse" 114 | :base-frequency "baseFrequency" 115 | :base-profile "baseProfile" 116 | :calc-mode "calcMode" 117 | :clip-path-units "clipPathUnits" 118 | :content-script-type "contentScriptType" 119 | :content-style-type "contentStyleType" 120 | :diffuse-constant "diffuseConstant" 121 | :edge-mode "edgeMode" 122 | :external-resources-required "externalResourcesRequired" 123 | :filter-res "filterRes" 124 | :filter-units "filterUnits" 125 | :glyph-ref "glyphRef" 126 | :gradient-transform "gradientTransform" 127 | :gradient-units "gradientUnits" 128 | :kernel-matrix "kernelMatrix" 129 | :kernel-unit-length "kernelUnitLength" 130 | :key-points "keyPoints" 131 | :key-splines "keySplines" 132 | :key-times "keyTimes" 133 | :length-adjust "lengthAdjust" 134 | :limiting-cone-angle "limitingConeAngle" 135 | :marker-height "markerHeight" 136 | :marker-units "markerUnits" 137 | :marker-width "markerWidth" 138 | :mask-content-units "maskContentUnits" 139 | :mask-units "maskUnits" 140 | :num-octaves "numOctaves" 141 | :path-length "pathLength" 142 | :pattern-content-units "patternContentUnits" 143 | :pattern-transform "patternTransform" 144 | :pattern-units "patternUnits" 145 | :points-at-x "pointsAtX" 146 | :points-at-y "pointsAtY" 147 | :points-at-z "pointsAtZ" 148 | :preserve-alpha "preserveAlpha" 149 | :preserve-aspect-ratio "preserveAspectRatio" 150 | :primitive-units "primitiveUnits" 151 | :ref-x "refX" 152 | :ref-y "refY" 153 | :repeat-count "repeatCount" 154 | :repeat-dur "repeatDur" 155 | :required-extensions "requiredExtensions" 156 | :required-features "requiredFeatures" 157 | :specular-constant "specularConstant" 158 | :specular-exponent "specularExponent" 159 | :spread-method "spreadMethod" 160 | :start-offset "startOffset" 161 | :std-deviation "stdDeviation" 162 | :stitch-tiles "stitchTiles" 163 | :surface-scale "surfaceScale" 164 | :system-language "systemLanguage" 165 | :table-values "tableValues" 166 | :target-x "targetX" 167 | :target-y "targetY" 168 | :view-box "viewBox" 169 | :view-target "viewTarget" 170 | :x-channel-selector "xChannelSelector" 171 | :xlink-actuate "xlink:actuate" 172 | :xlink-arcrole "xlink:arcrole" 173 | :xlink-href "xlink:href" 174 | :xlink-role "xlink:role" 175 | :xlink-show "xlink:show" 176 | :xlink-title "xlink:title" 177 | :xlink-type "xlink:type" 178 | :xml-base "xml:base" 179 | :xmlns-xlink "xmlns:xlink" 180 | :xml-lang "xml:lang" 181 | :xml-space "xml:space" 182 | :y-channel-selector "yChannelSelector" 183 | :zoom-and-pan "zoomAndPan"}) 184 | 185 | (defn get-value [attrs] 186 | (or (:value attrs) 187 | (:default-value attrs))) 188 | 189 | (defn normalize-attr-key ^String [key] 190 | (or (normalized-attrs key) 191 | (when (.startsWith (name key) "on") 192 | (-> (name key) (str/lower-case) (str/replace "-" ""))) 193 | (name key))) 194 | 195 | (defn escape-html [^String s] 196 | (let [len (count s)] 197 | (loop [^StringBuilder sb nil 198 | i (int 0)] 199 | (if (< i len) 200 | (let [char (.charAt s i) 201 | repl (case char 202 | \& "&" 203 | \< "<" 204 | \> ">" 205 | \" """ 206 | \' "'" 207 | nil)] 208 | (if (nil? repl) 209 | (if (nil? sb) 210 | (recur nil (inc i)) 211 | (recur (doto sb 212 | (.append char)) 213 | (inc i))) 214 | (if (nil? sb) 215 | (recur (doto (StringBuilder.) 216 | (.append s 0 i) 217 | (.append repl)) 218 | (inc i)) 219 | (recur (doto sb 220 | (.append repl)) 221 | (inc i))))) 222 | (if (nil? sb) s (str sb)))))) 223 | 224 | (defn parse-selector [s] 225 | (loop [matches (re-seq #"([#.])?([^#.]+)" (name s)) 226 | tag "div" 227 | id nil 228 | classes nil] 229 | (if-let [[_ prefix val] (first matches)] 230 | (case prefix 231 | nil (recur (next matches) val id classes) 232 | "#" (recur (next matches) tag val classes) 233 | "." (recur (next matches) tag id (conj (or classes []) val))) 234 | [tag id classes]))) 235 | 236 | (defn normalize-element [[first second & rest]] 237 | (when-not (or (keyword? first) 238 | (symbol? first) 239 | (string? first)) 240 | (throw (ex-info "Expected a keyword as a tag" {:tag first}))) 241 | (let [[tag tag-id tag-classes] (parse-selector first) 242 | [attrs children] (if (or (map? second) 243 | (nil? second)) 244 | [second rest] 245 | [nil (cons second rest)]) 246 | attrs-classes (:class attrs) 247 | classes (if (and tag-classes attrs-classes) 248 | [tag-classes attrs-classes] 249 | (or tag-classes attrs-classes))] 250 | [tag tag-id classes attrs children])) 251 | 252 | 253 | ;;; render attributes 254 | 255 | 256 | ;; https://github.com/facebook/react/blob/master/src/renderers/dom/shared/CSSProperty.js 257 | 258 | 259 | (def unitless-css-props 260 | (into #{} 261 | (for [key ["animation-iteration-count" "box-flex" "box-flex-group" "box-ordinal-group" "column-count" "flex" "flex-grow" "flex-positive" "flex-shrink" "flex-negative" "flex-order" "grid-row" "grid-column" "font-weight" "line-clamp" "line-height" "opacity" "order" "orphans" "tab-size" "widows" "z-index" "zoom" "fill-opacity" "stop-opacity" "stroke-dashoffset" "stroke-opacity" "stroke-width"] 262 | prefix ["" "-webkit-" "-ms-" "-moz-" "-o-"]] 263 | (str prefix key)))) 264 | 265 | (defn normalize-css-key [k] 266 | (-> (to-str k) 267 | (str/replace #"[A-Z]" (fn [ch] (str "-" (str/lower-case ch)))) 268 | (str/replace #"^ms-" "-ms-"))) 269 | 270 | (defn normalize-css-value [key value] 271 | (cond 272 | (contains? unitless-css-props key) (escape-html (str/trim (to-str value))) 273 | (number? value) (str value (when (not= 0 value) "px")) 274 | :else (escape-html (str/trim (to-str value))))) 275 | 276 | (defn render-style-kv! [sb empty? k v] 277 | (if v 278 | (do 279 | (if empty? 280 | (append! sb " style=\"") 281 | (append! sb ";")) 282 | (let [key (normalize-css-key k) 283 | val (normalize-css-value key v)] 284 | (append! sb key ":" val)) 285 | false) 286 | empty?)) 287 | 288 | (defn render-style! [map sb] 289 | (let [empty? (reduce-kv (partial render-style-kv! sb) true map)] 290 | (when-not empty? 291 | (append! sb "\"")))) 292 | 293 | (defn render-class! [sb first? class] 294 | (cond 295 | (nil? class) first? 296 | 297 | (string? class) 298 | (do 299 | (when-not first? 300 | (append! sb " ")) 301 | (append! sb (escape-html class)) 302 | false) 303 | 304 | (or (sequential? class) (set? class)) 305 | (reduce #(render-class! sb %1 %2) first? class) 306 | 307 | :else (render-class! sb first? (to-str class)))) 308 | 309 | (defn render-classes! [classes sb] 310 | (when classes 311 | (append! sb " class=\"") 312 | (render-class! sb true classes) 313 | (append! sb "\""))) 314 | 315 | (defn- render-attr-str! [sb attr value] 316 | (append! sb " " attr "=\"" (escape-html (to-str value)) "\"")) 317 | 318 | (defn render-attr! [tag key value sb] 319 | (let [attr (normalize-attr-key key)] 320 | (cond 321 | (= "type" attr) :nop ;; rendered manually in render-element! before id 322 | (= "style" attr) (render-style! value sb) 323 | (= "key" attr) :nop 324 | (= "ref" attr) :nop 325 | (= "class" attr) :nop 326 | (and (= "value" attr) 327 | (or (= "select" tag) 328 | (= "textarea" tag))) :nop 329 | (.startsWith attr "aria-") (render-attr-str! sb attr value) 330 | (not value) :nop 331 | (true? value) (append! sb " " attr "=\"\"") 332 | (.startsWith attr "on") (if (string? value) 333 | (render-attr-str! sb attr value) 334 | :nop) 335 | (= "dangerouslySetInnerHTML" attr) :nop 336 | :else (render-attr-str! sb attr value)))) 337 | 338 | (defn render-attrs! [tag attrs sb] 339 | (reduce-kv (fn [_ k v] (render-attr! tag k v sb)) nil attrs)) 340 | 341 | 342 | ;;; render html 343 | 344 | 345 | (defprotocol HtmlRenderer 346 | (-render-html [this *state sb] 347 | "Turn a Clojure data type into a string of HTML with react ids.")) 348 | 349 | (defn render-inner-html! [attrs children sb] 350 | (when-let [inner-html (:dangerouslySetInnerHTML attrs)] 351 | (when-not (empty? children) 352 | (throw (Exception. "Invariant Violation: Can only set one of `children` or `props.dangerouslySetInnerHTML`."))) 353 | (when-not (:__html inner-html) 354 | (throw (Exception. "Invariant Violation: `props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. Please visit https://fb.me/react-invariant-dangerously-set-inner-html for more information."))) 355 | (append! sb (:__html inner-html)) 356 | true)) 357 | 358 | (defn render-textarea-value! [tag attrs sb] 359 | (when (= tag "textarea") 360 | (when-some [value (get-value attrs)] 361 | (append! sb (escape-html value)) 362 | true))) 363 | 364 | (defn render-content! [tag attrs children *state sb] 365 | (if (and (nil? children) 366 | (contains? void-tags tag)) 367 | (append! sb "/>") 368 | (do 369 | (append! sb ">") 370 | (or (render-textarea-value! tag attrs sb) 371 | (render-inner-html! attrs children sb) 372 | (doseq [child children] 373 | (-render-html child *state sb))) 374 | (append! sb ""))) 375 | (when (not= :state/static @*state) 376 | (vreset! *state :state/tag-close))) 377 | 378 | (defn render-element! 379 | "Render an element vector as a HTML element." 380 | [element *state sb] 381 | (when-not (nothing? element) 382 | (let [[tag id classes attrs children] (normalize-element element)] 383 | (if (or (= "*" tag) 384 | (= "<>" tag)) 385 | ;; React Fragment 386 | (-render-html children *state sb) 387 | (do 388 | (append! sb "<" tag) 389 | 390 | (when-some [type (:type attrs)] 391 | (append! sb " type=\"" (escape-html (to-str type)) "\"")) 392 | 393 | (when (and (= "option" tag) 394 | (or (= (get-value attrs) *select-value*) 395 | (and *multiple?* 396 | (set? *select-value*) 397 | (contains? *select-value* (get-value attrs))) 398 | (and *multiple?* 399 | (sequential? *select-value*) 400 | (->> *select-value* 401 | (filter #(= (get-value attrs) %)) 402 | (seq))))) 403 | (append! sb " selected=\"\"")) 404 | 405 | (when id 406 | (append! sb " id=\"" id "\"")) 407 | 408 | (render-attrs! tag attrs sb) 409 | 410 | (render-classes! classes sb) 411 | 412 | (when (= :state/root @*state) 413 | (append! sb " data-reactroot=\"\"")) 414 | 415 | (when (not= :state/static @*state) 416 | (vreset! *state :state/tag-open)) 417 | 418 | (if (= "select" tag) 419 | (binding [*select-value* (get-value attrs) 420 | *multiple?* (:multiple attrs)] 421 | (render-content! tag attrs children *state sb)) 422 | (render-content! tag attrs children *state sb))))))) 423 | 424 | (deftype JSComponent [s] 425 | HtmlRenderer 426 | (-render-html [this *state sb] 427 | (append! sb s))) 428 | 429 | (extend-protocol HtmlRenderer 430 | IPersistentVector 431 | (-render-html [this *state sb] 432 | (render-element! this *state sb)) 433 | 434 | ISeq 435 | (-render-html [this *state sb] 436 | (when (= :state/root @*state) 437 | (vreset! *state :state/root-seq)) 438 | (doseq [element this] 439 | (-render-html element *state sb))) 440 | 441 | String 442 | (-render-html [this *state sb] 443 | (when (= @*state :state/text) 444 | (append! sb "")) 445 | (append! sb (escape-html this)) 446 | (when (not= :state/static @*state) 447 | (vreset! *state :state/text))) 448 | 449 | Object 450 | (-render-html [this *state sb] 451 | (-render-html (str this) *state sb)) 452 | 453 | nil 454 | (-render-html [this *state sb] 455 | :nop)) 456 | 457 | (defn render-html 458 | ([src] (render-html src nil)) 459 | ([src opts] 460 | (let [sb (StringBuilder.)] 461 | (-render-html src (volatile! :state/root) sb) 462 | (str sb)))) 463 | 464 | (defn render-static-markup [src] 465 | (let [sb (StringBuilder.)] 466 | (-render-html src (volatile! :state/static) sb) 467 | (str sb))) 468 | -------------------------------------------------------------------------------- /src/rum/specs.cljc: -------------------------------------------------------------------------------- 1 | (ns rum.specs) 2 | 3 | (def mixins 4 | #{:init :will-mount :before-render :wrap-render :did-mount 5 | :after-render :did-remount :will-remount :should-update :will-update 6 | :did-update :did-catch :will-unmount :child-context 7 | :class-properties :static-properties :key-fn 8 | :unsafe/will-mount :unsafe/will-update}) 9 | 10 | (def deprecated-mixins 11 | {:did-remount ":did-remount is deprecated and was renamed to :will-remount, semantics didn't change, it was always called in componentWillReceiveProps"}) 12 | -------------------------------------------------------------------------------- /src/rum/util.cljc: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc rum.util) 2 | 3 | (defn collect [key mixins] 4 | (into [] 5 | (keep (fn [m] (get m key))) 6 | mixins)) 7 | 8 | (defn collect* [keys mixins] 9 | (into [] 10 | (mapcat (fn [m] (keep (fn [k] (get m k)) keys))) 11 | mixins)) 12 | 13 | (defn call-all [state fns & args] 14 | (reduce 15 | (fn [state fn] 16 | (apply fn state args)) 17 | state 18 | fns)) -------------------------------------------------------------------------------- /test/daiquiri/interpreter_test.cljs: -------------------------------------------------------------------------------- 1 | (ns daiquiri.interpreter-test 2 | (:require [clojure.test :refer [are is deftest]] 3 | [daiquiri.interpreter :refer [interpret]])) 4 | 5 | ;; Ported from https://github.com/r0man/sablono/blob/master/test/sablono/interpreter_test.cljc 6 | 7 | (deftest test-short-hand-div-forms 8 | (let [el (interpret [:#test.klass1])] 9 | (is (= "div" (.. el -type))) 10 | (is (= "test" (.. el -props -id))) 11 | (is (= "klass1" (.. el -props -className))))) 12 | 13 | (deftest test-static-children-as-arguments 14 | (let [el (interpret 15 | [:div 16 | [:div {:class "1" :key 1}] 17 | [:div {:class "2" :key 2}]]) 18 | c1 (aget (.. el -props -children) 0) 19 | c2 (aget (.. el -props -children) 1)] 20 | (is (= "div" (.. el -type))) 21 | 22 | (is (= "div" (.. c1 -type))) 23 | (is (= "1" (.. c1 -key))) 24 | (is (= "1" (.. c1 -props -className))) 25 | 26 | (is (= "div" (.. c2 -type))) 27 | (is (= "2" (.. c2 -key))) 28 | (is (= "2" (.. c2 -props -className))))) 29 | 30 | (deftest test-class-duplication 31 | (let [el (interpret [:div.a.a.b.b.c {:class "c"}])] 32 | (is (= "div" (.. el -type))) 33 | (is (= "a a b b c c" (.. el -props -className))))) 34 | 35 | (deftest test-issue-80 36 | (let [el (interpret 37 | [:div 38 | [:div {:class (list "foo" "bar")}] 39 | [:div {:class (vector "foo" "bar")}] 40 | (let [] 41 | [:div {:class (list "foo" "bar")}]) 42 | (let [] 43 | [:div {:class (vector "foo" "bar")}]) 44 | (when true 45 | [:div {:class (list "foo" "bar")}]) 46 | (when true 47 | [:div {:class (vector "foo" "bar")}]) 48 | (do 49 | [:div {:class (list "foo" "bar")}]) 50 | (do 51 | [:div {:class (vector "foo" "bar")}])])] 52 | (is (= "div" (.. el -type))) 53 | (is (= 8 (count (.. el -props -children)))) 54 | (doseq [c (.. el -props -children)] 55 | (is (= "div" (.. c -type))) 56 | (is (= "foo bar" (.. c -props -className)))))) 57 | 58 | (deftest test-issue-90 59 | (let [el (interpret [:div nil (case :a :a "a")])] 60 | (is (= "div" (.. el -type))) 61 | (is (= "a" (.. el -props -children))))) 62 | 63 | (deftest test-issue-57 64 | (let [payload {:username "john" :likes 2} 65 | el (interpret 66 | (let [{:keys [username likes]} payload] 67 | [:div 68 | [:div (str username " (" likes ")")] 69 | [:div "!Pixel Scout"]])) 70 | c1 (aget (.. el -props -children) 0) 71 | c2 (aget (.. el -props -children) 1)] 72 | 73 | (is (= "div" (.. el -type))) 74 | 75 | (is (= "div" (.. c1 -type))) 76 | (is (= "john (2)" (.. c1 -props -children))) 77 | 78 | (is (= "div" (.. c2 -type))) 79 | (is (= "!Pixel Scout" (.. c2 -props -children))))) 80 | 81 | (deftest test-fragment 82 | (let [el (interpret [:<> 1 2])] 83 | (is (= js/React.Fragment (.. el -type))) 84 | (is (= 1 (aget (.. el -props -children) 0))) 85 | (is (= 2 (aget (.. el -props -children) 1)))) 86 | (let [el (interpret [:<> {:key 11} 1 2])] 87 | (is (= js/React.Fragment (.. el -type))) 88 | (is (= "11" (.-key el))) 89 | (is (= 1 (aget (.. el -props -children) 0))) 90 | (is (= 2 (aget (.. el -props -children) 1))))) 91 | 92 | (deftest test-interop 93 | (let [c (fn [^js props] (.-x props)) 94 | el1 (interpret [:> c {:x 1 :class [1 2] :style {:y 2}} 2]) 95 | el2 (interpret [:> c 2])] 96 | (is (= c (.. el1 -type))) 97 | (is (= 1 (.. el1 -props -x))) 98 | (is (= [1 2] (.. el1 -props -class))) 99 | (is (= {:y 2} (.. el1 -props -style))) 100 | (is (= 2 (.. el1 -props -children))) 101 | 102 | (is (= c (.. el2 -type))) 103 | (is (= 2 (.. el2 -props -children))))) 104 | 105 | (deftest test-235 106 | (let [c (interpret [:div [[:div] [:div]]])] 107 | (is (= "div" (.. c -type))) 108 | (is (= 2 (count (.. c -props -children)))))) 109 | -------------------------------------------------------------------------------- /test/daiquiri/normalize_test.cljc: -------------------------------------------------------------------------------- 1 | (ns daiquiri.normalize-test 2 | (:require [daiquiri.normalize :as normalize] 3 | #?(:clj [clojure.test :refer :all] 4 | :cljs [cljs.test :refer-macros [are is deftest]]))) 5 | 6 | ;; Ported from https://github.com/r0man/sablono/blob/master/test/sablono/normalize_test.cljc 7 | 8 | (deftest test-merge-with-class 9 | (are [maps expected] 10 | (= expected (apply normalize/merge-with-class maps)) 11 | [] 12 | nil 13 | [{:a 1} {:b 2}] 14 | {:a 1 :b 2} 15 | [{:a 1 :class :a} {:b 2 :class "b"} {:c 3 :class ["c"]}] 16 | {:a 1 :b 2 :c 3 :class ["a" "b" "c"]} 17 | [{:a 1 :class :a} {:b 2 :class "b"} {:c 3 :class (seq ["c"])}] 18 | {:a 1 :b 2 :c 3 :class ["a" "b" "c"]} 19 | ['{:a 1 :class ["a"]} '{:b 2 :class [(if true "b")]}] 20 | '{:a 1 :class ["a" (if true "b")] :b 2} 21 | ;; Map lookup. Issue #130 22 | ['{:class (:table-cell csslib)} {}] 23 | '{:class [(:table-cell csslib)]})) 24 | 25 | (deftest test-strip-css 26 | (are [x expected] 27 | (= expected (normalize/strip-css x)) 28 | nil nil 29 | "" "" 30 | "foo" "foo" 31 | "#foo" "foo" 32 | ".foo" "foo")) 33 | 34 | (deftest test-match-tag 35 | (are [tag expected] 36 | (= expected (normalize/match-tag tag)) 37 | :div ["div" nil []] 38 | :div#foo ["div" "foo" []] 39 | :div#foo.bar ["div" "foo" ["bar"]] 40 | :div.bar#foo ["div" "foo" ["bar"]] 41 | :div#foo.bar.baz ["div" "foo" ["bar" "baz"]] 42 | :div.bar.baz#foo ["div" "foo" ["bar" "baz"]] 43 | :div.bar#foo.baz ["div" "foo" ["bar" "baz"]]) 44 | (let [[tag id classes] (normalize/match-tag :div#foo.bar.baz)] 45 | (is (= "div" tag)) 46 | (is (= "foo" id)) 47 | (is (= ["bar" "baz"] classes)) 48 | (is (vector? classes)))) 49 | 50 | (deftest test-normalize-class 51 | (are [class expected] 52 | (= expected (normalize/normalize-class class)) 53 | nil nil 54 | :x ["x"] 55 | "x" ["x"] 56 | ["x"] ["x"] 57 | [:x] ["x"] 58 | '(if true "x") ['(if true "x")] 59 | 'x ['x] 60 | '("a" "b") ["a" "b"])) 61 | 62 | (deftest test-attributes 63 | (are [attrs expected] 64 | (= expected (normalize/attributes attrs)) 65 | nil nil 66 | {} {} 67 | {:class nil} {:class nil} 68 | {:class "x"} {:class ["x"]} 69 | {:class ["x"]} {:class ["x"]} 70 | '{:class ["x" (if true "y")]} '{:class ["x" (if true "y")]})) 71 | 72 | (deftest test-children 73 | (are [children expected] 74 | (= expected (normalize/children children)) 75 | [] [] 76 | 1 [1] 77 | "x" ["x"] 78 | ["x"] ["x"] 79 | [["x"]] ["x"] 80 | [["x" "y"]] ["x" "y"] 81 | [:div] [[:div]] 82 | [[:div]] [[:div]] 83 | [[[:div]]] [[:div]])) 84 | 85 | (deftest test-element 86 | (are [element expected] 87 | (= expected (normalize/element element)) 88 | [:div] ["div" {} '()] 89 | [:div {:class nil}] ["div" {:class nil} '()] 90 | [:div#foo] ["div" {:id "foo"} '()] 91 | [:div.foo] ["div" {:class ["foo"]} '()] 92 | [:div.a.b] ["div" {:class ["a" "b"]} '()] 93 | [:div.a.b {:class "c"}] ["div" {:class ["a" "b" "c"]} '()] 94 | [:div.a.b {:class nil}] ["div" {:class ["a" "b"]} '()] 95 | [:div "a" "b"] ["div" {} ["a" "b"]] 96 | [:div ["a" "b"]] ["div" {} ["a" "b"]])) 97 | 98 | (deftest test-element-meta 99 | (are [element expected] 100 | (= (->> (nth (normalize/element element) 2) 101 | (map (comp true? :inline meta))) 102 | expected) 103 | '[:span (constantly 1)] [false] 104 | '[:span ^:inline (constantly 1)] [true] 105 | '[:span ^:inline (constantly 1) nil ^:inline (constantly 2)] [true true])) 106 | -------------------------------------------------------------------------------- /test/daiquiri/util_test.cljc: -------------------------------------------------------------------------------- 1 | (ns daiquiri.util-test 2 | (:require [daiquiri.util :as u] 3 | #?(:clj [clojure.test :refer :all]) 4 | #?(:cljs [cljs.test :refer-macros [are is testing deftest]]))) 5 | 6 | ;; Ported from https://github.com/r0man/sablono/blob/master/test/sablono/util_test.cljc 7 | 8 | (deftest test-camel-case 9 | (are [attr expected] 10 | (= expected (u/camel-case attr)) 11 | nil nil 12 | "" "" 13 | :data :data 14 | :data-toggle :data-toggle 15 | :http-equiv :httpEquiv 16 | :aria-checked :aria-checked 17 | '(identity :class) '(identity :class))) 18 | 19 | (deftest test-camel-case-keys 20 | (are [attrs expected] 21 | (= expected (u/camel-case-keys attrs)) 22 | {:id "x"} 23 | {:id "x"} 24 | {:class "x"} 25 | {:class "x"} 26 | {:http-equiv "Expires"} 27 | {:httpEquiv "Expires"} 28 | {:style {:z-index 1000}} 29 | {:style {:zIndex 1000}} 30 | {:on-click '(fn [e] (let [m {:a-b "c"}]))} 31 | {:onClick '(fn [e] (let [m {:a-b "c"}]))} 32 | {'(identity :class) "my-class" 33 | :style {:background-color "black"}} 34 | {'(identity :class) "my-class" 35 | :style {:backgroundColor "black"}})) 36 | 37 | (deftest test-html-to-dom-attrs 38 | (are [attrs expected] 39 | (= expected (u/html-to-dom-attrs attrs)) 40 | {:id "x"} 41 | {:id "x"} 42 | {:class "x"} 43 | {:className "x"} 44 | {:http-equiv "Expires"} 45 | {:httpEquiv "Expires"} 46 | {:style {:z-index 1000}} 47 | {:style {:zIndex 1000}} 48 | {:on-click '(fn [e] (let [m {:a-b "c"}]))} 49 | {:onClick '(fn [e] (let [m {:a-b "c"}]))} 50 | {'(identity :class) "my-class" 51 | :style {:background-color "black"}} 52 | {'(identity :class) "my-class" 53 | :style {:backgroundColor "black"}})) 54 | 55 | (deftest test-element? 56 | (is (u/element? [:div])) 57 | (is (not (u/element? nil))) 58 | (is (not (u/element? []))) 59 | (is (not (u/element? 1))) 60 | (is (not (u/element? "x")))) 61 | 62 | (deftest test-join-classes 63 | (are [classes expected] 64 | (= expected (u/join-classes classes)) 65 | ["a"] "a" 66 | #{"a"} "a" 67 | ["a" "b"] "a b" 68 | #{"a" "b"} "a b" 69 | ["a" ["b"]] "a b" 70 | ["a" (set ["a" "b" "c"])] "a a b c")) 71 | -------------------------------------------------------------------------------- /test/rum/test/cursor.clj: -------------------------------------------------------------------------------- 1 | (ns rum.test.cursor 2 | (:require 3 | [rum.core :as rum] 4 | [clojure.test :refer [deftest is are testing]])) 5 | 6 | (deftest test-cursor 7 | (let [a (atom {:b 1 :c {:cd 2}}) 8 | b (rum/cursor a :b) 9 | c (rum/cursor a :c) 10 | cd (rum/cursor c :cd) 11 | ccd (rum/cursor-in a [:c :cd])] 12 | (testing "deref" 13 | (is (= 1 @b)) 14 | (is (= {:cd 2} @c)) 15 | (is (= 2 @cd)) 16 | (is (= 2 @ccd))) 17 | 18 | (testing "swap!" 19 | (is (= 2 (swap! b inc))) 20 | (is (= 2 @b)) 21 | (is (= 3 (swap! cd inc))) 22 | (is (= 3 @cd))) 23 | 24 | (testing "reset!" 25 | (is (= 4 (reset! b 4))) 26 | (is (= 4 @b)) 27 | (is (= 5 (reset! cd 5))) 28 | (is (= 5 @cd))) 29 | 30 | (testing "compare-and-set!" 31 | (is (= true (compare-and-set! b 4 6))) 32 | (is (= 6 @b)) 33 | (is (= false (compare-and-set! b 4 7))) 34 | (is (= 6 @b)) 35 | (is (= true (compare-and-set! cd 5 8))) 36 | (is (= 8 @cd)) 37 | (is (= false (compare-and-set! cd 5 9))) 38 | (is (= 8 @cd))) 39 | 40 | (testing "watches" 41 | (let [b-count (atom 0) 42 | b-states (atom []) 43 | _ (add-watch b ::count (fn [_ _ _ _] (swap! b-count inc))) 44 | _ (add-watch b ::count (fn [_ _ _ _] (swap! b-count inc))) ;; duplicate add 45 | _ (add-watch b ::states (fn [_ _ o n] (swap! b-states conj [o n]))) 46 | cd-count (atom 0) 47 | _ (add-watch cd ::count (fn [_ _ _ _] (swap! cd-count inc)))] 48 | 49 | (= 2 (count (.getWatches b))) 50 | (= 1 (count (.getWatches cd))) 51 | 52 | (swap! b inc) 53 | (is (= 1 @b-count)) 54 | (is (= [[6 7]] @b-states)) 55 | (is (= 0 @cd-count)) 56 | 57 | (swap! cd inc) 58 | (is (= 1 @b-count)) 59 | (is (= [[6 7]] @b-states)) 60 | (is (= 1 @cd-count)) 61 | 62 | (remove-watch b ::count) 63 | (= 1 (count (.getWatches b))) 64 | (swap! b inc) 65 | (is (= 1 @b-count)) 66 | (is (= [[6 7] [7 8]] @b-states))))) 67 | 68 | (testing "meta" 69 | (let [c (rum/cursor (atom nil) :b :meta {:k 1})] 70 | (is (= {:k 1} (meta c))) 71 | (alter-meta! c update :k inc) 72 | (is (= {:k 2} (meta c))) 73 | (reset-meta! c {:l 3}) 74 | (is (= {:l 3} (meta c))))) 75 | 76 | (testing "vectors" 77 | (let [a (atom [1 [2 3] {:k 4}]) 78 | b (rum/cursor a 0) 79 | c (rum/cursor-in a [1 0]) 80 | d (rum/cursor-in a [2 :k])] 81 | (is (= 1 @b)) 82 | (is (= 2 @c)) 83 | (is (= 4 @d)) 84 | (swap! b inc) 85 | (swap! c inc) 86 | (swap! d inc) 87 | (is (= [2 [3 3] {:k 5}] @a))))) 88 | 89 | -------------------------------------------------------------------------------- /test/rum/test/defc.clj: -------------------------------------------------------------------------------- 1 | (ns rum.test.defc 2 | (:require 3 | [rum.core] 4 | [clojure.test :refer [deftest is are testing]] 5 | [clojure.walk :refer [prewalk]])) 6 | 7 | (defn replace-gensyms [sym forms] 8 | (prewalk 9 | (fn [form] 10 | (if (and (symbol? form) 11 | (re-matches (re-pattern (str sym "\\d+")) (str form))) 12 | sym 13 | form)) 14 | forms)) 15 | 16 | (defmacro eval-in-temp-ns [& forms] 17 | `(binding [*ns* *ns*] 18 | (in-ns (gensym)) 19 | (clojure.core/use 'clojure.core) 20 | (clojure.core/use 'rum.core) 21 | (eval 22 | '(do ~@forms)))) 23 | 24 | ;; Copied from Clojure: https://git.io/vwFsG 25 | (deftest defc-error-messages 26 | (testing "bad name" 27 | (is (thrown-with-msg? 28 | IllegalArgumentException 29 | #"First argument to defc must be a symbol" 30 | (eval-in-temp-ns (defc "bad docstring" testname [arg1 arg2]))))) 31 | (testing "mixins after argvec" 32 | (is (thrown-with-msg? 33 | IllegalArgumentException 34 | #"Mixins must be given before argument list" 35 | (eval-in-temp-ns (defc testname "docstring" [arg1 arg2] < misplaced-mixin)))) 36 | (is (thrown-with-msg? 37 | IllegalArgumentException 38 | #"Mixins must be given before argument list" 39 | (eval-in-temp-ns (defc testname "docstring" 40 | ([arg1] < misplaced-mixin) 41 | ([arg1 arg2] < misplaced-mixin))))) 42 | (is (thrown-with-msg? 43 | IllegalArgumentException 44 | #"Mixins must be given before argument list" 45 | (eval-in-temp-ns (defc testname 46 | ([arg1] < misplaced-mixin) 47 | ([arg1 arg2] < misplaced-mixin))))))) 48 | 49 | (deftest defc-conditions 50 | (testing "no conditions supplied" 51 | (is (= (replace-gensyms 'pre-post-test 52 | (#'rum.core/-defc 'rum.core/build-defc 53 | {:ns {:name 'core}} ; cljs? 54 | '(pre-post-test ([y] {:x 1}) 55 | ([y z] (+ y z 1))))) 56 | '(def pre-post-test 57 | (rum.core/lazy-build rum.core/build-defc 58 | (clojure.core/fn 59 | ([y] (do {:x 1})) 60 | ([y z] (do (daiquiri.compiler/interpret-maybe (+ y z 1))))) 61 | nil 62 | "core/pre-post-test"))))) 63 | 64 | (testing "some conditions supplied" 65 | (is (= (replace-gensyms 'pre-post-test 66 | (#'rum.core/-defc 'rum.core/build-defc 67 | {:ns {:name 'core}} ; cljs? 68 | '(pre-post-test ([y] {:pre [(pos? y)]} {:x 1}) 69 | ([y z] (+ y z 1))))) 70 | '(def pre-post-test 71 | (rum.core/lazy-build rum.core/build-defc 72 | (clojure.core/fn 73 | ([y] {:pre [(pos? y)]} (do {:x 1})) 74 | ([y z] (do (daiquiri.compiler/interpret-maybe (+ y z 1))))) 75 | nil 76 | "core/pre-post-test")))))) 77 | -------------------------------------------------------------------------------- /test/rum/test/derived_atom.clj: -------------------------------------------------------------------------------- 1 | (ns rum.test.derived-atom 2 | (:require 3 | [rum.core :as rum] 4 | [clojure.test :refer [deftest is are testing]])) 5 | 6 | (deftest test-derived 7 | (let [*a (atom 0) 8 | *b (atom "x") 9 | *d (rum/derived-atom [*a *b] ::key 10 | (fn [a b] 11 | (str a ":" b)))] 12 | (is (= "0:x" @*d)) 13 | (swap! *a inc) 14 | (is (= "1:x" @*d)) 15 | (reset! *b "y") 16 | (is (= "1:y" @*d))) 17 | 18 | (testing "user-provided ref" 19 | (let [*a (atom 0) 20 | *d (atom nil)] 21 | (rum/derived-atom [*a] ::key str {:ref *d}) 22 | (is (= "0" @*d)) 23 | (swap! *a inc) 24 | (is (= "1" @*d)))) 25 | 26 | (testing "check-equals" 27 | (let [*a (atom 0) 28 | *d (atom nil) 29 | *resets (atom 0)] 30 | (add-watch *d ::count-resets (fn [_ _ _ _] (swap! *resets inc))) 31 | (rum/derived-atom [*a] ::key #(mod % 10) {:ref *d}) 32 | (is (= 0 @*d)) 33 | (is (= 1 @*resets)) 34 | (reset! *a 1) 35 | (is (= 1 @*d)) 36 | (is (= 2 @*resets)) 37 | (reset! *a 11) ;; *a changes, 38 | (is (= 1 @*d)) ;; but *d does not 39 | (is (= 2 @*resets))) ;; should not register reset! 40 | 41 | (let [*a (atom 0) 42 | *d (atom nil) 43 | *resets (atom 0)] 44 | (add-watch *d ::count-resets (fn [_ _ _ _] (swap! *resets inc))) 45 | (rum/derived-atom [*a] ::key #(mod % 10) {:ref *d :check-equals? false}) 46 | (is (= 0 @*d)) 47 | (is (= 1 @*resets)) 48 | (reset! *a 1) 49 | (is (= 1 @*d)) 50 | (is (= 2 @*resets)) 51 | (reset! *a 11) 52 | (is (= 1 @*d)) 53 | (is (= 3 @*resets))))) ;; should register reset! anyways 54 | 55 | -------------------------------------------------------------------------------- /test/rum/test/react_render_html.js: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/node 2 | 3 | var fs = require('fs'), 4 | vm = require('vm'); 5 | 6 | global.goog = {}; 7 | 8 | global.CLOSURE_IMPORT_SCRIPT = function(src) { 9 | require('./target/none/goog/' + src); 10 | return true; 11 | }; 12 | 13 | function nodeGlobalRequire(file) { 14 | vm.runInThisContext.call(global, fs.readFileSync(file), file); 15 | } 16 | 17 | nodeGlobalRequire('./target/test.js'); 18 | 19 | rum.test.server_render.react_render_html(fs.writeFileSync); 20 | -------------------------------------------------------------------------------- /test/rum/test/server.clj: -------------------------------------------------------------------------------- 1 | (ns rum.test.server 2 | (:require 3 | [rum.core :as rum] 4 | [clojure.test :refer [deftest is are testing]])) 5 | 6 | (rum/defcs comp-mixins < (rum/local 7) 7 | {:will-mount (fn [s] (assoc s ::key 1))} 8 | [state] 9 | [:div 10 | [:.local @(:rum/local state)] 11 | [:.key (::key state)]]) 12 | 13 | (deftest test-js-component-ssr 14 | (binding [rum/*render-js-component* (fn [type-sym attrs children] 15 | (case type-sym 16 | 'h2 (rum/render-static-markup (into [:h2 attrs] children)) 17 | nil))] 18 | (is (= (rum/render-static-markup 19 | (rum/adapt-class h2 {:tab-index "1"} "This is JS component")) 20 | "

This is JS component

")))) 21 | 22 | (deftest test-93 23 | (is (= (rum/render-static-markup [:a {:type "