├── .editorconfig ├── .gitattributes ├── .gitignore ├── .idea └── codeStyleSettings.xml ├── CHANGES.md ├── CITATION.md ├── CONTRIBUTING.md ├── README.md ├── bin ├── doctoc.bat └── doctoc.sh ├── circle.yml ├── docs ├── ApplicationState.md ├── Basic-App-Structure.md ├── Code-Of-Conduct.md ├── CodeWalkthrough.md ├── Coeffects.md ├── Debugging-Event-Handlers.md ├── Debugging.md ├── EffectfulHandlers.md ├── Effects.md ├── EventHandlingInfographic.md ├── External-Resources.md ├── FAQs │ ├── CatchingEventExceptions.md │ ├── Inspecting-app-db.md │ ├── Logging.md │ ├── Null-Dispatched-Events.md │ ├── README.md │ ├── UseASubscriptionInAnEventHandler.md │ ├── Why-CLJC.md │ └── Why-Clear-Sub-Cache.md ├── Figma Infographics │ └── inforgraphics.fig ├── Interceptors.md ├── Loading-Initial-Data.md ├── MentalModelOmnibus.md ├── Namespaced-Keywords.md ├── Navigation.md ├── Performance-Problems.md ├── README.md ├── Solve-the-CPU-hog-problem.md ├── Subscribing-To-External-Data.md ├── SubscriptionFlow.md ├── SubscriptionInfographic.md ├── SubscriptionsCleanup.md ├── Talking-To-Servers.md ├── Testing.md ├── The-re-frame-logo.md ├── Using-Stateful-JS-Components.md └── re-frankenstein │ └── about-effects.md ├── examples ├── simple │ ├── README.md │ ├── project.clj │ ├── resources │ │ └── public │ │ │ ├── example.css │ │ │ └── example.html │ └── src │ │ └── simple │ │ └── core.cljs └── todomvc │ ├── .gitignore │ ├── README.md │ ├── project.clj │ ├── resources │ └── public │ │ ├── frank-todos.css │ │ ├── index.html │ │ └── todos.css │ └── src │ └── todomvc │ ├── core.cljs │ ├── db.cljs │ ├── events.cljs │ ├── frank_subs.cljs │ ├── frank_views.cljs │ ├── subs.cljs │ └── views.cljs ├── images ├── Readme │ ├── 6dominoes.png │ ├── Dominoes-small.jpg │ ├── Dominoes.jpg │ └── todolist.png ├── event-handlers.png ├── example_app.png ├── logo │ ├── Genesis.png │ ├── Guggenheim.jpg │ ├── README.md │ ├── frame_1024w.png │ ├── re-frame-logo.sketch │ ├── re-frame_128w.png │ ├── re-frame_256w.png │ ├── re-frame_512w.png │ └── re-frankenstein-logo.png ├── mental-model-omnibus.jpg ├── scale-changes-everything.jpg ├── subscriptions.png └── the-water-cycle.png ├── karma.conf.js ├── license.txt ├── project.clj ├── re-frankenstein-CHANGES.md ├── src └── re_frame │ ├── cofx.cljc │ ├── core.cljc │ ├── db.cljc │ ├── events.cljc │ ├── frank.cljs │ ├── fx.cljc │ ├── interceptor.cljc │ ├── interop.clj │ ├── interop.cljs │ ├── loggers.cljc │ ├── registrar.cljc │ ├── router.cljc │ ├── rum.cljs │ ├── std_interceptors.cljc │ ├── subs.cljc │ ├── trace.cljc │ └── utils.cljc └── test ├── re-frame ├── event_test.cljs ├── frank_test.cljs ├── fx_test.cljs ├── interceptor_test.cljs ├── restore_test.cljs ├── rum_test.cljs ├── subs_test.cljs ├── test_runner.cljs └── trace_test.cljs └── test.html /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [package.json] 11 | indent_style = space 12 | indent_size = 2 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | CHANGES.md merge=union 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/.idea/**/* 2 | !.idea/codeStyleSettings.xml 3 | *.iml 4 | *.log 5 | .nrepl-port 6 | pom.xml 7 | pom.xml.asc 8 | *.jar 9 | .lein-deps-sum 10 | .lein-repl-history 11 | .lein-plugins/ 12 | .lein-failures 13 | core/ 14 | out/ 15 | target/ 16 | compiled/ 17 | misc/ 18 | /examples/todomvc/resources/public/js/ 19 | /examples/simple/resources/public/js/ 20 | .floo 21 | .flooignore 22 | node_modules/ 23 | examples/todomvc/.idea/ 24 | -------------------------------------------------------------------------------- /.idea/codeStyleSettings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 21 | -------------------------------------------------------------------------------- /CITATION.md: -------------------------------------------------------------------------------- 1 | To cite re-frame in publications, please use: 2 | 3 | Thompson, M. (2015, March). Re-Frame: A Reagent Framework For Writing SPAs, in Clojurescript. 4 | Zenodo. http://doi.org/10.5281/zenodo.xxxxx 5 | 6 | @misc{thompson_2015, 7 | author = {Thompson, Michael}, 8 | title = {Re-Frame: A Reagent Framework For Writing SPAs, in Clojurescript.}, 9 | month = mar, 10 | year = 2015, 11 | doi = {10.5281/zenodo.xxxx}, 12 | url = {https://doi.org/10.5281/zenodo.xxxxx} 13 | } 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to re-frame 2 | 3 | Thank you for taking the time to contribute! Please note that re-frame follows a [code of conduct](docs/Code-Of-Conduct.md). 4 | 5 | ## Support questions 6 | 7 | The Github issues are for bug reports and feature requests only. Support requests and usage 8 | questions should go to the re-frame [Clojure Slack channel](http://clojurians.net) or 9 | the [ClojureScript mailing list](https://groups.google.com/forum/#!forum/clojurescript). 10 | 11 | ## Pull requests 12 | 13 | **Create pull requests to the develop branch**, work will be merged onto master when it is ready to be released. 14 | 15 | ## Running tests 16 | 17 | #### Via Browser/HTML 18 | 19 | To build the tests and run them in one step, just: 20 | ```sh 21 | lein test-once # compiles & then opens test.html in the browser 22 | ``` 23 | 24 | You can also get auto compiles via: 25 | ```sh 26 | lein test-auto 27 | ``` 28 | but you'll need to manually open `test/test.html` in a browser. And you'll also need to 29 | manually reload this page after each auto compile. 30 | 31 | #### Via Karma 32 | 33 | To run the tests, you must have recent versions of node, npm, Leiningen, and a C++ compiler 34 | toolchain installed. If you're on Linux or Mac OS X then you will be fine, 35 | if you're on Windows then you need to install Visual Studio Community Edition, 36 | and the C++ compiler dependencies. 37 | 38 | ```sh 39 | npm install karma-cli -g # Install the Karma CLI runner 40 | lein deps # runs lein-npm, installs Karma & other node dependencies. Only needed the first time. 41 | lein karma-once # to build re-frame tests 42 | karma start # to run the tests with an auto watcher 43 | ``` 44 | 45 | ## Pull requests for bugs 46 | 47 | If possible provide: 48 | 49 | * Code that fixes the bug 50 | * Failing tests which pass with the new changes 51 | * Improvements to documentation to make it less likely that others will run into issues (if relevant). 52 | * Add the change to the Unreleased section of [CHANGES.md](CHANGES.md) 53 | 54 | ## Pull requests for features 55 | 56 | If possible provide: 57 | 58 | * Code that implements the new feature 59 | * Tests to cover the new feature including all of the code paths 60 | * Docstrings for functions 61 | * Documentation examples 62 | * Add the change to the Unreleased section of [CHANGES.md](CHANGES.md) 63 | 64 | ## Pull requests for docs 65 | 66 | * Make your documentation changes 67 | * (Optional) Install doctoc with `npm install -g doctoc` 68 | * (Optional) Regenerate the docs TOC with `bin/doctoc.sh` or `bin/doctoc.bat` depending on your OS 69 | -------------------------------------------------------------------------------- /bin/doctoc.bat: -------------------------------------------------------------------------------- 1 | :: Table of contents are generated by doctoc. 2 | :: Install doctoc with `npm install -g doctoc` 3 | :: Then run this script to regenerate the TOC after 4 | :: editing the docs. 5 | 6 | doctoc ./docs/ --github --title '## Table Of Contents' 7 | -------------------------------------------------------------------------------- /bin/doctoc.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Table of contents are generated by doctoc. 3 | # Install doctoc with `npm install -g doctoc` 4 | # Then run this script to regenerate the TOC after 5 | # editing the docs. 6 | 7 | doctoc $(dirname $0)/../docs --github --title '## Table Of Contents' 8 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | dependencies: 2 | pre: 3 | - npm install karma-cli -g 4 | cache_directories: 5 | - node_modules 6 | test: 7 | override: 8 | - lein karma-once 9 | - karma start --single-run --reporters junit,dots 10 | -------------------------------------------------------------------------------- /docs/ApplicationState.md: -------------------------------------------------------------------------------- 1 | ## Application State 2 | 3 |
4 |

Well-formed Data at rest is as close to perfection in programming as it gets. All the crap that had to happen to put it there however...

— Fogus (@fogus) April 11, 2014
5 |
6 | 7 | ### The Big Ratom 8 | 9 | re-frame puts all your application state into one place, which is 10 | called `app-db`. 11 | 12 | Ideally, you will provide a spec for this data-in-the-one-place, 13 | [using a powerful and leverageable schema](http://clojure.org/about/spec). 14 | 15 | Now, this advice is not the slightest bit controversial for 'real' databases, right? 16 | You'd happily put all your well-formed data into PostgreSQL. 17 | 18 | But within a running application (in memory), there can be hesitation. If you have 19 | a background in OO, this data-in-one-place 20 | business is a really, really hard one to swallow. You've 21 | spent your life breaking systems into pieces, organised around behaviour and trying 22 | to hide state. I still wake up in a sweat some nights thinking about all 23 | that Clojure data lying around exposed and passive. 24 | 25 | But, as Fogus reminds us, data at rest is quite perfect. 26 | 27 | In re-frame, `app-db` is one of these: 28 | ```clj 29 | (def app-db (reagent/atom {})) ;; a Reagent atom, containing a map 30 | ``` 31 | 32 | Although it is a `Reagent atom` (hereafter `ratom`), I'd encourage 33 | you to think of it as an in-memory database. It will contain structured data. 34 | You will need to query that data. You will perform CRUD 35 | and other transformations on it. You'll often want to transact on this 36 | database atomically, etc. So "in-memory database" 37 | seems a more useful paradigm than plain old map-in-atom. 38 | 39 | Further Notes: 40 | 41 | 1. `app-state` would probably be a more accurate name, but I choose `app-db` instead because 42 | I wanted to convey the in-memory database notion as strongly as possible. 43 | 2. In the documentation and code, I make a distinction between `app-db` (the `ratom`) and 44 | `db` which is the (map) `value` currently stored **inside** this `ratom`. Be aware of that naming as you read code. 45 | 3. re-frame creates and manages an `app-db` for you, so 46 | you don't need to declare one yourself (see the [the first FAQ](FAQs/Inspecting-app-db.md) if you want 47 | to inspect the value it holds). 48 | 4. `app-db` doesn't actually have to be a `ratom` containing a map. It could, for example, 49 | be a [datascript database](https://github.com/tonsky/datascript). In fact, any database which 50 | can signal you when it changes would do. We'd love! to be using [datascript database](https://github.com/tonsky/datascript) - so damn cool - 51 | but we had too much data in our apps. If you were to use it, you'd have to tweak re-frame a bit and use [Posh](https://github.com/mpdairy/posh). 52 | 53 | 54 | ### The Benefits Of Data-In-The-One-Place 55 | 56 | 1. Here's the big one: because there is a single source of truth, we write no 57 | code to synchronise state between many different stateful components. I 58 | cannot stress enough how significant this is. You end up writing less code 59 | and an entire class of bugs is eliminated. 60 | (This mindset is very different to OO which involves 61 | distributing state across objects, and then ensuring that state is synchronised, all the while 62 | trying to hide it, which is, when you think about it, quite crazy ... and I did it for years). 63 | 64 | 2. Because all app state is coalesced into one atom, it can be updated 65 | with a single `reset!`, which acts like a transactional commit. There is 66 | an instant in which the app goes from one state to the next, never a series 67 | of incremental steps which can leave the app in a temporarily inconsistent, intermediate state. 68 | Again, this simplicity causes a certain class of bugs or design problems to evaporate. 69 | 70 | 3. The data in `app-db` can be given a strong schema 71 | so that, at any moment, we can validate all the data in the application. **All of it!** 72 | We do this check after every single "event handler" runs (event handlers compute new state). 73 | And this enables us to catch errors early (and accurately). It increases confidence in the way 74 | that Types can increase confidence, only [a good schema can potentially provide more 75 | **leverage** than types](https://www.youtube.com/watch?v=nqY4nUMfus8). 76 | 77 | 4. Undo/Redo [becomes straight forward to implement](https://github.com/Day8/re-frame-undo). 78 | It is easy to snapshot and restore one central value. Immutable data structures have a 79 | feature called `structural sharing` which means it doesn't cost much RAM to keep the last, say, 200 80 | snapshots. All very efficient. 81 | For certain categories of applications (eg: drawing applications) this feature is borderline magic. 82 | Instead of undo/redo being hard, disruptive and error prone, it becomes trivial. 83 | **But,** many web applications are not self contained 84 | data-wise and, instead, are dominated by data sourced from an authoritative, remote database. 85 | For these applications, re-frame's `app-db` is mostly a local caching 86 | point, and being able to do undo/redo its state is meaningless because the authoritative 87 | source of data is elsewhere. 88 | 89 | 5. The ability to genuinely model control via FSMs (discussed later). 90 | 91 | 6. The ability to do time travel debugging, even in a production setting. More soon. 92 | 93 | 94 | ### Create A Leveragable Schema 95 | 96 | You need to create a [spec](http://clojure.org/about/spec) schema for `app-db`. You want that leverage. 97 | 98 | Of course, that means you'll have to learn [spec](http://clojure.org/about/spec) and there's 99 | some overhead in that, so maybe, just maybe, in your initial experiments, you can 100 | get away without one. But not for long. Promise me you'll write a `spec`. Promise me. Okay, good. 101 | 102 | Soon we'll look at the [todomvc example](https://github.com/Day8/re-frame/tree/master/examples/todomvc) 103 | which shows how to use a spec. (Check out `src/db.cljs` for the spec itself, and then in `src/events.cljs` for 104 | how to write code which checks `app-db` against this spec after every single event has been 105 | processed.) 106 | 107 | Specs are potentially more leveragable than types. This is a big interesting idea which is not yet mainstream. 108 | Watch how:
109 | https://www.youtube.com/watch?v=VNTQ-M_uSo8 110 | 111 | Also, watch the mighty Rich Hickey (poor audio):
112 | https://vimeo.com/195711510 113 | 114 | ### How do I inspect it? 115 | 116 | See [FAQ #1](FAQs/Inspecting-app-db.md) 117 | 118 | *** 119 | 120 | Previous: [This Repo's README](../README.md)       121 | Up: [Index](README.md)       122 | Next: [First Code Walk-Through](CodeWalkthrough.md) 123 | 124 | 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /docs/Basic-App-Structure.md: -------------------------------------------------------------------------------- 1 | 2 | ## Simpler Apps 3 | 4 | To build a re-frame app, you: 5 | - design your app's data structures (data layer) 6 | - write Reagent view functions (domino 5) 7 | - write event handler functions (control layer and/or state transition layer, domino 2) 8 | - write subscription functions (query layer, domino 4) 9 | 10 | For simpler apps, you should put code for each layer into separate files: 11 | ``` 12 | src 13 | ├── core.cljs <--- entry point, plus history, routing, etc 14 | ├── db.cljs <--- schema, validation, etc (data layer) 15 | ├── views.cljs <--- reagent views (view layer) 16 | ├── events.cljs <--- event handlers (control/update layer) 17 | └── subs.cljs <--- subscription handlers (query layer) 18 | ``` 19 | 20 | For a living example of this approach, look at the [todomvc example](https://github.com/Day8/re-frame/tree/master/examples/todomvc). 21 | 22 | *No really, you should absolutely look at the [todomvc example](https://github.com/Day8/re-frame/tree/master/examples/todomvc) example, as soon as possible. It contains all sorts of tips.* 23 | 24 | ### There's A Small Gotcha 25 | 26 | If you adopt this structure, there's a gotcha. 27 | 28 | `events.cljs` and `subs.cljs` will never be `required` by any other 29 | namespaces. To the Google Closure dependency mechanism it appears as 30 | if these two namespaces are not needed and it doesn't load them. 31 | 32 | And, if the code does not get loaded, the registrations in these namespaces 33 | never happen. You'll then you'll be puzzled as to why none of your events handlers 34 | are registered. 35 | 36 | Once you twig to what's going on, the solution is easy. You must 37 | explicitly `require` both namespaces, `events` and `subs`, in your `core` 38 | namespace. Then they'll be loaded and the registrations will occur 39 | as that loading happens. 40 | 41 | ## Larger Apps 42 | 43 | Assuming your larger apps have multiple "panels" (or "views") which are 44 | relatively independent, you might use this structure: 45 | ``` 46 | src 47 | ├── panel-1 48 | │ ├── db.cljs <--- schema, validation, etc (data layer) 49 | │ ├── subs.cljs <--- subscription handlers (query layer) 50 | │ ├── views.cljs <--- reagent components (view layer) 51 | │ └── events.cljs <--- event handlers (control/update layer) 52 | ├── panel-2 53 | │ ├── db.cljs <--- schema, validation. etc (data layer) 54 | │ ├── subs.cljs <--- subscription handlers (query layer) 55 | │ ├── views.cljs <--- reagent components (view layer) 56 | │ └── events.cljs <--- event handlers (control/update layer) 57 | . 58 | . 59 | └── panel-n 60 | ``` 61 | 62 | *** 63 | 64 | Previous: [Correcting a wrong](SubscriptionsCleanup.md)       65 | Up: [Index](README.md)       66 | Next: [Navigation](Navigation.md) 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /docs/Code-Of-Conduct.md: -------------------------------------------------------------------------------- 1 | # Open Source Code of Conduct 2 | 3 | In order to foster an inclusive, kind, harassment-free, and cooperative community, Day8 enforces this code of conduct on our open source projects. 4 | 5 | ## Summary 6 | 7 | Harassment in code and discussion or violation of physical boundaries is completely unacceptable anywhere in Day8’s project codebases, issue trackers, chatrooms, mailing lists, meetups, and other events. Violators will be warned by the core team. Repeat violations will result in being blocked or banned by the core team at or before the 3rd violation. 8 | 9 | ## In detail 10 | 11 | Harassment includes offensive verbal comments related to gender identity, gender expression, sexual orientation, disability, physical appearance, body size, race, religion, sexual images, deliberate intimidation, stalking, sustained disruption, and unwelcome sexual attention. 12 | 13 | Individuals asked to stop any harassing behavior are expected to comply immediately. 14 | 15 | Maintainers are also subject to the anti-harassment policy. 16 | 17 | If anyone engages in harassing behavior, including maintainers, we may take appropriate action, up to and including warning the offender, deletion of comments, removal from the project’s codebase and communication systems, and escalation to GitHub support. 18 | 19 | If you are being harassed, notice that someone else is being harassed, or have any other concerns, please contact a member of the core team or email conduct@day8.com.au immediately. 20 | 21 | We expect everyone to follow these rules anywhere in Day8's project codebases, issue trackers, chatrooms, and mailing lists. 22 | 23 | Finally, don't forget that it is human to make mistakes! We all do. Let’s work together to help each other, resolve issues, and learn from the mistakes that we will all inevitably make from time to time. 24 | 25 | ## Thanks 26 | Thanks to the [Thoughtbot Code of Conduct][thoughtbot], [CocoaPods Code of Conduct][cocoapods], [Bundler Code of Conduct][bundler], [JSConf Code of Conduct][jsconf], and [Contributor Covenant][contributor] for inspiration and ideas. 27 | 28 | ## License 29 | To the extent possible under law, the Day8 team has waived all copyright and related or neighboring rights to Day8 Code of Conduct. This work is published from Australia. 30 | 31 | CC0 licensed 32 | 33 | [thoughtbot]: https://thoughtbot.com/open-source-code-of-conduct 34 | [cocoapods]: https://github.com/CocoaPods/CocoaPods/blob/master/CODE_OF_CONDUCT.md 35 | [bundler]: http://bundler.io/conduct.html 36 | [jsconf]: http://jsconf.com/codeofconduct.html 37 | [contributor]: http://contributor-covenant.org/ 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /docs/EventHandlingInfographic.md: -------------------------------------------------------------------------------- 1 | 2 | ## Event Handling Infographics 3 | 4 | Three diagrams are provided below: 5 | - a beginner's romp 6 | - an intermediate schematic depiction 7 | - an advanced, full detail rendering 8 | 9 | They should be reviewed in conjunction with the written tutorials. 10 | 11 | 12 | 13 | *** 14 | 15 | Previous: [Mental Model Omnibus](MentalModelOmnibus.md)       16 | Up: [Index](README.md)       17 | Next: [Effectful Handlers](EffectfulHandlers.md)       18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /docs/External-Resources.md: -------------------------------------------------------------------------------- 1 | ## External Resources 2 | 3 | Please add to this list by submitting a pull request. 4 | 5 | 6 | ### Templates 7 | 8 | * [re-frame-template](https://github.com/Day8/re-frame-template) - Generates the client side SPA 9 | 10 | * [Luminus](http://www.luminusweb.net) - Generates SPA plus server side. 11 | 12 | * [re-natal](https://github.com/drapanjanas/re-natal) - React Native apps. 13 | 14 | * [Slush-reframe](https://github.com/kristianmandrup/slush-reframe) - A scaffolding generator for re-frame run using NodeJS. Should work wih re-frame `0.7.0` if used on a project started from the `0.7.0` version of re-frame-template. 15 | 16 | * [Celibidache](https://github.com/velveteer/celibidache/) - An opinionated starter for re-frame applications using Boot. Based on re-frame `0.7.0` 17 | 18 | 19 | ### Examples and Applications Using re-frame 20 | 21 | * [How to create decentralised apps with re-frame and Ethereum](https://medium.com/@matus.lestan/how-to-create-decentralised-apps-with-clojurescript-re-frame-and-ethereum-81de24d72ff5#.b9xh9xnis) - Tutorial with links to code and live example. 22 | 23 | * [Elfeed-cljsrn](https://github.com/areina/elfeed-cljsrn) - A mobile client for [Elfeed](https://github.com/skeeto/elfeed) rss reader, built with React Native. 24 | 25 | * [Memory Hole](https://github.com/yogthos/memory-hole) - A small issue tracking app written with Luminus and re-frame. 26 | 27 | * [Crossed](https://github.com/velveteer/crossed/) - A multiplayer crossword puzzle generator. Based on re-frame `0.7.0` 28 | 29 | * [imperimetric](https://github.com/Dexterminator/imperimetric) - Webapp for converting texts with some system of measurement to another, such as imperial to metric. 30 | 31 | * [Brave Clojure Open Source](https://github.com/braveclojure/open-source) A site using re-frame, liberator, boot and more to display active github projects that powers [http://open-source.braveclojure.com](http://open-source.braveclojure.com). Based on re-frame `0.6.0` 32 | 33 | * [flux-challenge with re-frame](https://github.com/staltz/flux-challenge/tree/master/submissions/jelz) - flux-challenge is "a frontend challenge to test UI architectures and solutions". This is a ClojureScript + re-frame version. Based on re-frame `0.5.0` 34 | 35 | * [fractalify](https://github.com/madvas/fractalify/) - 36 | An entertainment and educational webapp for creating & sharing fractal images that powers [fractalify.com](http://fractalify.com). Based on re-frame `0.4.1` 37 | 38 | * [Angular Phonecat tutorial in re-frame](http://dhruvp.github.io/2015/03/07/re-frame/) - A detailed step-by-step tutorial that ports the Angular Phonecat tutorial to re-frame. Based on re-frame `0.2.0` 39 | 40 | * [Braid](https://github.com/braidchat/braid) - A new approach to group chat, designed around conversations and tags instead of rooms. 41 | 42 | ### Effect and CoEffect Handlers 43 | 44 | * [async-flow-fx](https://github.com/Day8/re-frame-async-flow-fx) - manage a boot process dominated by async 45 | * [http-fx](https://github.com/Day8/re-frame-http-fx) - performing Ajax tasks (via cljs-ajax) 46 | * [re-frame-forward-events-fx](https://github.com/Day8/re-frame-forward-events-fx) - slightly exotic 47 | * [cookie-fx](https://github.com/SMX-LTD/re-frame-cookie-fx) - set and get cookies 48 | * [document-fx](https://github.com/SMX-LTD/re-frame-document-fx) - set and get on `js/document` attributes 49 | * [re-frame-youtube-fx](https://github.com/micmarsh/re-frame-youtube-fx) - YouTube iframe API wrapper 50 | * [re-frame-web3-fx](https://github.com/madvas/re-frame-web3-fx) - Ethereum Web3 API 51 | * [re-frame-google-analytics-fx](https://github.com/madvas/re-frame-google-analytics-fx) - Google Analytics API 52 | 53 | ### Routing 54 | 55 | * (Bidirectional using Silk and Pushy)[https://pupeno.com/2015/08/18/no-hashes-bidirectional-routing-in-re-frame-with-silk-and-pushy/] 56 | 57 | ### Tools, Techniques & Libraries 58 | 59 | * [re-frame-undo](https://github.com/Day8/re-frame-undo) - An undo library for re-frame 60 | * Animation using `react-flip-move` - http://www.upgradingdave.com/blog/posts/2016-12-17-permutation.html 61 | * [re-frisk](https://github.com/flexsurfer/re-frisk) - A library for visualizing re-frame data and events. 62 | * [re-thread](https://github.com/yetanalytics/re-thread) - A library for running re-frame applications in Web Workers. 63 | * [re-frame-datatable](https://github.com/kishanov/re-frame-datatable) - DataTable UI component built for use with re-frame. 64 | * [Stately: State Machines](https://github.com/nodename/stately) also https://www.youtube.com/watch?v=klqorRUPluw 65 | * [re-frame-test](https://github.com/Day8/re-frame-test) - Integration Testing (not documented) 66 | * [re-learn](https://github.com/oliyh/re-learn) - Data driven tutorials for educating users of your reagent / re-frame app, built with re-frame 67 | 68 | ### Videos 69 | 70 | * [re-frame your ClojureScript applications](https://youtu.be/cDzjlx6otCU) - re-frame presentation given at Clojure/Conj 2016 71 | 72 | * [A Video Tour of the Source Code of Ninja Tools](https://carouselapps.com/2015/12/02/tour-of-the-source-code-of-ninja-tools/) 73 | 74 | ### Server Side Rendering 75 | 76 | * [Prerenderer](https://github.com/pupeno/prerenderer) - Server pre-rendering library using NodeJS that works with re-frame `0.6.0` (later versions untested) 77 | Rationale [Part 1](https://carouselapps.com/2015/09/14/isomorphic-clojurescriptjavascript-for-pre-rendering-single-page-applications-part-2/) 78 | [Part 2](https://carouselapps.com/2015/09/14/isomorphic-clojurescriptjavascript-for-pre-rendering-single-page-applications-part-2/) 79 | [Part 3](https://pupeno.com/2015/10/02/isomorphic-javascript-with-clojurescript-for-pre-rendering-single-page-applications-part-3/) 80 | [Release Announcement](https://pupeno.com/2015/12/13/prerenderer-0-2-0-released/) 81 | 82 | * [Server Side Rendering with re-frame](http://davidtanzer.net/server_side_rendering_with_re_frame) - Blog post on rendering re-frame views with Clojure. 83 | 84 | * [Rendering Reagent on the Server Using Hiccup](http://yogthos.net/posts/2015-11-24-Serverside-Reagent.html)- Blog post on rendering Reagent with Clojure. 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /docs/FAQs/CatchingEventExceptions.md: -------------------------------------------------------------------------------- 1 | ### Question 2 | 3 | How can I detect exceptions in Event Handlers? 4 | 5 | ### Answer 6 | 7 | A suggested solution can be found in [this issue](https://github.com/Day8/re-frame/issues/231#issuecomment-249991378). 8 | 9 | *** 10 | 11 | Up: [FAQ Index](README.md)       12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /docs/FAQs/Inspecting-app-db.md: -------------------------------------------------------------------------------- 1 | ### Question 2 | 3 | How can I inspect the contents of `app-db`? Perhaps from figwheel. 4 | 5 | ### Short Answer 6 | 7 | If at a REPL, inspect: `re-frame.db/app-db`. 8 | 9 | If at the js console, that's `window.re_frame.db.app_db.state`. 10 | 11 | You are [using cljs-devtools](https://github.com/binaryage/cljs-devtools), right? 12 | If not, stop everything and immediately make that happen. 13 | 14 | ### Better Answer 15 | 16 | Are you sure you need to? 17 | 18 | First, you seldom want to inspect all of `app-db`. 19 | And, second, inspecting via a REPL might be clumsy. 20 | 21 | Instead, you probably want to inspect a part of `app-db`. __And__ you probably want 22 | to inspect it directly in the GUI itself, not off in a REPL. 23 | 24 | Here is a useful technique from @escherize. Add something like this to 25 | the hiccup of your view ... 26 | ```clj 27 | [:pre (with-out-str (pprint @interesting))] 28 | ``` 29 | This assumes that `@interesting` is the value (ratom or subscription) 30 | you want to observe (note the @ in front). 31 | 32 | `pprint` output is nice to read, but not compact. For a more compact view, do this: 33 | ```clj 34 | [:pre (pr-str @some-atom)] ;; NB: using pr-str instead of pprint 35 | ``` 36 | 37 | If you choose to use `pprint` then you'll need to `require` it within the `ns` of your `view.cljs`: 38 | ```clj 39 | [cljs.pprint :refer [pprint]] 40 | ``` 41 | 42 | Finally, combining the short and long answers, you could even do this: 43 | ```clj 44 | [:pre (with-out-str (pprint @re-frame.db/app-db))] ;; see everything! 45 | ``` 46 | or 47 | ```clj 48 | [:pre (with-out-str (pprint (:part @re-frame.db/app-db)))] ;; see a part of it! 49 | ``` 50 | 51 | You definitely have [clj-devtools](https://github.com/binaryage/cljs-devtools) installed now, right? 52 | 53 | ### Other Inspection Tools 54 | 55 | Another very strong tool is [re-Frisk](https://github.com/flexsurfer/re-frisk) which 56 | provides a nice solution for navigating and inspecting your re-frame data structures. 57 | 58 | @yogthos' [json-html library](https://github.com/yogthos/json-html) provides 59 | a slick presentation, at the expense of more screen real estate, and the 60 | need to include specific CSS. 61 | 62 | *** 63 | 64 | Up: [FAQ Index](README.md)       65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /docs/FAQs/Logging.md: -------------------------------------------------------------------------------- 1 | ### Question 2 | 3 | I use logging method X, how can I make re-frame use my method? 4 | 5 | ### Answer 6 | 7 | re-frame makes use of the logging functions: `warn`, `log`, `error`, `group` and `groupEnd`. 8 | 9 | By default, these functions map directly to the js/console equivalents, but you can 10 | override that by providing your own set or subset of these functions using 11 | `re-frame.core/set-loggers!` like this: 12 | ```clj 13 | (defn my-warn 14 | [& args] 15 | (post-warning-somewhere (apply str args))) 16 | 17 | (defn my-log 18 | [& args] 19 | (write-to-datadog (apply str args))) 20 | 21 | (re-frame.core/set-loggers! {:warn my-warn 22 | :log my-log 23 | ...}) 24 | ``` 25 | 26 | *** 27 | 28 | Up: [FAQ Index](README.md)       29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /docs/FAQs/Null-Dispatched-Events.md: -------------------------------------------------------------------------------- 1 | ### Question 2 | 3 | If I `dispatch` a js event object (from a view), it is nullified 4 | by the time it gets to the event-handler. What gives? 5 | 6 | ### Answer 7 | 8 | So there's two things to say about this: 9 | - if you want to `dispatch` a js event object to a re-frame 10 | event handler, you must call `(.persist event)` before the `dispatch`. 11 | React recycles events (using a pool), and re-frame event handlers 12 | run async. [Find out more here](https://facebook.github.io/react/docs/events.html) 13 | 14 | - it is probably more idiomatic to extract the salient data from the event 15 | and `dispatch` that, rather than the js event object itself. When you 16 | `dispatch` pure, simple cljs data (ie. rather than js objects) testing 17 | and debugging will become easier. 18 | 19 | 20 | *** 21 | 22 | Up: [FAQ Index](README.md)       23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /docs/FAQs/README.md: -------------------------------------------------------------------------------- 1 | ## Frequently Asked Questions 2 | 3 | 1. [How can I Inspect app-db?](Inspecting-app-db.md) 4 | 2. [How can I use a subscription in an Event Handler](UseASubscriptionInAnEventHandler.md) 5 | 2. [How do I use logging method X](Logging.md) 6 | 3. [Dispatched Events Are Null](Null-Dispatched-Events.md) 7 | 4. [Why implement re-frame in `.cljc` files](Why-CLJC.md) 8 | 5. [Why do we need to clear the subscription cache when reloading with Figwheel?](Why-Clear-Sub-Cache.md) 9 | 6. [How can I detect exceptions in Event Handlers?](CatchingEventExceptions.md) 10 | 11 | 12 | 13 | ## Want To Add An FAQ? 14 | 15 | We'd like that. Please supply a PR. Or just open an issue. Many Thanks!! 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /docs/FAQs/UseASubscriptionInAnEventHandler.md: -------------------------------------------------------------------------------- 1 | ### Question 2 | 3 | How do I access the value of a subscription from inside an event handler? 4 | 5 | ### Short Answer 6 | 7 | You shouldn't. 8 | 9 | Philosophically: subscriptions are designed to deliver "a stream" of new values 10 | over time. Within an event handler, we only need a one off value. 11 | 12 | Operationally: you'll end up with a memory leak. 13 | 14 | ### Longer Answer 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /docs/FAQs/Why-CLJC.md: -------------------------------------------------------------------------------- 1 | ### Question 2 | 3 | Why is re-frame implemented in `.cljc` files? Aren't ClojureScript 4 | files meant to be `.cljs`? 5 | 6 | ### Answer 7 | 8 | So tests can be run on both the JVM and the JS platforms, 9 | re-frame's implementation is mostly in `.cljc` files. 10 | 11 | The trailing `c` in `.cljc` stands for `common`. 12 | 13 | Necessary interop for each platform can be found in 14 | `interop.clj` (for the JVM) and `interop.cljs` (for JS). 15 | 16 | See also: https://github.com/Day8/re-frame-test 17 | 18 | 19 | *** 20 | 21 | Up: [FAQ Index](README.md)       22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /docs/FAQs/Why-Clear-Sub-Cache.md: -------------------------------------------------------------------------------- 1 | ### Question 2 | 3 | Why do we call `clear-subscription-cache!` when reloading code with Figwheel? 4 | 5 | ### Answer 6 | 7 | Pour yourself a drink, as this is a circuitous tale involving one of the hardest 8 | problems in Computer Science. 9 | 10 | **1: Humble beginnings** 11 | 12 | When React is rendering, if an exception is thrown, it doesn't catch or 13 | handle the errors gracefully. Instead, all of the React components up to 14 | the root are destroyed. When these components are destroyed, none of their 15 | standard lifecycle methods are called, like `ComponentDidUnmount`. 16 | 17 | 18 | **2: Simple assumptions** 19 | 20 | Reagent tracks the watchers of a Reaction to know when no-one is watching and 21 | it can call the Reaction's `on-dispose`. Part of the book-keeping involved in 22 | this requires running the `on-dispose` in a React `ComponentWillUnmount` lifecycle 23 | method. 24 | 25 | At this point, your spidey senses are probably tingling. 26 | 27 | **3: The hardest problem in CS** 28 | 29 | re-frame subscriptions are created as Reactions. re-frame helpfully deduplicates 30 | subscriptions if multiple parts of the view request the same subscription. This 31 | is a big efficiency boost. When re-frame creates the subscription Reaction, it 32 | sets the `on-dispose` method of that subscription to remove itself from the 33 | subscription cache. This means that when that subscription isn't being watched 34 | by any part of the view, it can be disposed. 35 | 36 | **4: The gnarly implications** 37 | 38 | If you are 39 | 40 | 1. Writing a re-frame app 41 | 2. Write a bug in your subscription code (your one bug for the year) 42 | 3. Which causes an exception to be thrown in your rendering code 43 | 44 | then: 45 | 46 | 1. React will destroy all of the components in your view without calling `ComponentWillUnmount`. 47 | 2. Reagent will not get notified that some subscriptions are not needed anymore. 48 | 3. The subscription on-dispose functions that should have been run, are not. 49 | 4. re-frame's subscription cache will not be invalidated correctly, and the subscription with the bug 50 | is still in the cache. 51 | 52 | At this point you are looking at a blank screen. After debugging, you find the problem and fix it. 53 | You save your code and Figwheel recompiles and reloads the changed code. Figwheel attempts to re-render 54 | from the root. This causes all of the Reagent views to be rendered and to request re-frame subscriptions 55 | if they need them. Because the old buggy subscription is still sitting around in the cache, re-frame 56 | will return that subscription instead of creating a new one based on the fixed code. The only way around 57 | this (once you realise what is going on) is to reload the page. 58 | 59 | **5: Coda** 60 | 61 | re-frame 0.9.0 provides a new function: `re-frame.core/clear-subscription-cache!` which will run the 62 | on-dispose function for every subscription in the cache, emptying the cache, and causing new subscriptions 63 | to be created after reloading. 64 | 65 | *** 66 | 67 | Up: [FAQ Index](README.md)       68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /docs/Figma Infographics/inforgraphics.fig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chpill/re-frankenstein/c3b84969088f78aa00f4cfcef13338d8cb9a279e/docs/Figma Infographics/inforgraphics.fig -------------------------------------------------------------------------------- /docs/Namespaced-Keywords.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Table Of Contents 4 | 5 | - [Namespaced Ids](#namespaced-ids) 6 | 7 | 8 | 9 | ## Namespaced Ids 10 | 11 | As an app gets bigger, you'll tend to get clashes on ids - event-ids, or query-ids (subscriptions), etc. 12 | 13 | One panel will need to `dispatch` an `:edit` event and so will 14 | another, but the two panels will have different handlers. 15 | So how then to not have a clash? How then to distinguish between 16 | one `:edit` event and another? 17 | 18 | Your goal should be to use event-ids which encode both the event 19 | itself (`:edit` ?) and the context (`:panel1` or `:panel2` ?). 20 | 21 | Luckily, ClojureScript provides a nice easy solution: use keywords 22 | with a __synthetic namespace__. Perhaps something like `:panel1/edit` and `:panel2/edit`. 23 | 24 | You see, ClojureScript allows the namespace in a keyword to be a total 25 | fiction. I can have the keyword `:panel1/edit` even though 26 | `panel1.cljs` doesn't exist. 27 | 28 | Naturally, you'll take advantage of this by using keyword namespaces 29 | which are both unique and descriptive. 30 | 31 | *** 32 | 33 | Previous: [Navigation](Navigation.md)       34 | Up: [Index](README.md)       35 | Next: [Loading Initial Data](Loading-Initial-Data.md) 36 | -------------------------------------------------------------------------------- /docs/Navigation.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Table Of Contents 4 | 5 | - [What About Navigation?](#what-about-navigation) 6 | 7 | 8 | 9 | 10 | ## What About Navigation? 11 | 12 | How do I switch between different panels of a larger app? 13 | 14 | Your `app-db` could have an `:active-panel` key containing an id for the panel being displayed. 15 | 16 | 17 | When the user does something navigation-ish (selects a tab, a dropdown or something which changes the active panel), then the associated event and dispatch look like this: 18 | 19 | ```clj 20 | (re-frame/reg-event-db 21 | :set-active-panel 22 | (fn [db [_ value]] 23 | (assoc db :active-panel value))) 24 | 25 | (re-frame/dispatch 26 | [:set-active-panel :panel1]) 27 | ``` 28 | 29 | A high level reagent view has a subscription to :active-panel and will switch to the associated panel. 30 | 31 | ```clj 32 | (re-frame/reg-sub 33 | :active-panel 34 | (fn [db _] 35 | (:active-panel db))) 36 | 37 | (defn panel1 38 | [] 39 | [:div {:on-click #(re-frame/dispatch [:set-active-panel :panel2])} 40 | "Here" ]) 41 | 42 | (defn panel2 43 | [] 44 | [:div "There"]) 45 | 46 | (defn high-level-view 47 | [] 48 | (let [active (re-frame/subscribe [:active-panel])] 49 | (fn [] 50 | [:div 51 | [:div.title "Heading"] 52 | (condp = @active ;; or you could look up in a map 53 | :panel1 [panel1] 54 | :panel2 [panel2])]))) 55 | ``` 56 | 57 | 58 | Continue to [Namespaced Keywords](Namespaced-Keywords.md) to reduce clashes on ids. 59 | 60 | *** 61 | 62 | Previous: [Basic App Structure](Basic-App-Structure.md)       63 | Up: [Index](README.md)       64 | Next: [Namespaced Keywords](Namespaced-Keywords.md) 65 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ### Introduction 2 | 3 | - [This Repo's README](../README.md) 4 | - [app-db (Application State)](ApplicationState.md) 5 | - [First Code Walk-Through](CodeWalkthrough.md) 6 | - [Mental Model Omnibus](MentalModelOmnibus.md) 7 | 8 | 9 | ### Event Handlers 10 | 11 | - [Infographic Overview](EventHandlingInfographic.md) 12 | - [Effectful Handlers](EffectfulHandlers.md) 13 | - [Interceptors](Interceptors.md) 14 | - [Effects](Effects.md) 15 | - [Coeffects](Coeffects.md) 16 | 17 | ### Subscriptions 18 | 19 | - [Infographic](SubscriptionInfographic.md) 20 | - [Correcting a wrong](SubscriptionsCleanup.md) 21 | - [Flow Mechanics](SubscriptionFlow.md) 22 | 23 | ### App Structure 24 | 25 | - [Basic App Structure](Basic-App-Structure.md) 26 | - [Navigation](Navigation.md) 27 | - [Namespaced Keywords](Namespaced-Keywords.md) 28 | 29 | 30 | ### App Data 31 | 32 | - [Loading Initial Data](Loading-Initial-Data.md) 33 | - [Talking To Servers](Talking-To-Servers.md) 34 | - [Subscribing to External Data](Subscribing-To-External-Data.md) 35 | 36 | 37 | ### Debugging And Testing 38 | 39 | - [Debugging Event Handlers](Debugging-Event-Handlers.md) 40 | - [Debugging](Debugging.md) 41 | - [Testing](Testing.md) 42 | 43 | 44 | ### Miscellaneous 45 | - [FAQs](FAQs/README.md) 46 | - [External Resources](External-Resources.md) 47 | - [Eek! Performance Problems](Performance-Problems.md) 48 | - [Solve the CPU hog problem](Solve-the-CPU-hog-problem.md) 49 | - [Using Stateful JS Components](Using-Stateful-JS-Components.md) 50 | - [The re-frame Logo](The-re-frame-logo.md) 51 | - [Code Of Conduct](Code-Of-Conduct.md) 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /docs/SubscriptionInfographic.md: -------------------------------------------------------------------------------- 1 | ## Subscription Infographic 2 | 3 | There's two things to do here. 4 | 5 | **First**, please read through the 6 | annotated subscription code [in the todomvc example](https://github.com/Day8/re-frame/blob/master/examples/todomvc/src/todomvc/subs.cljs). 7 | 8 | **Then**, look at this Infographic: 9 | 10 | 11 | 12 | *** 13 | 14 | Previous: [Coeffects](Coeffects.md)       15 | Up: [Index](README.md)       16 | Next: [Correcting a wrong](SubscriptionsCleanup.md)       17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /docs/SubscriptionsCleanup.md: -------------------------------------------------------------------------------- 1 | ## Subscriptions Cleanup 2 | 3 | There's a problem and we need to fix it. 4 | 5 | 6 | ### The Problem 7 | 8 | The simple example, used in the earlier code walk through, is not idomatic re-frame. It has a flaw. 9 | 10 | It does not obey the re-frame rule: **keep views as simple as possible**. 11 | 12 | A view shouldn't do any computation on input data. Its job is just to compute hiccup. 13 | The subscriptions it uses should deliver the data already in the right 14 | structure, ready for use in hiccup generation. 15 | 16 | ### Just Look 17 | 18 | Here be the horrors: 19 | ```clj 20 | (defn clock 21 | [] 22 | [:div.example-clock 23 | {:style {:color @(rf/subscribe [:time-color])}} 24 | (-> @(rf/subscribe [:time]) 25 | .toTimeString 26 | (clojure.string/split " ") 27 | first)]) 28 | ``` 29 | 30 | That view obtains data from a `[:time]` subscription and then it 31 | massages that data into the form it needs for use in the hiccup. We don't like that. 32 | 33 | ### The Solution 34 | 35 | Instead, we want to use a new `[:time-str]` subscription which will deliver the data all ready to go, so 36 | the view is 100% concerned with hiccup generation only. Like this: 37 | ```clj 38 | (defn clock 39 | [] 40 | [:div.example-clock 41 | {:style {:color @(rf/subscribe [:time-color])}} 42 | @(rf/subscribe [:time-str])]) 43 | ``` 44 | 45 | Which, in turn, means we must write this `time-str` subscription handler: 46 | ```clj 47 | (reg-sub 48 | :time-str 49 | (fn [_ _] 50 | (subscribe [:time])) 51 | (fn [t _] 52 | (-> t 53 | .toTimeString 54 | (clojure.string/split " ") 55 | first))) 56 | ``` 57 | 58 | Much better. 59 | 60 | You'll notice this new subscription handler belongs to the "Level 3" 61 | layer of the reactive flow. See the [Infographic](SubscriptionInfographic.md). 62 | 63 | ### Another Technique 64 | 65 | Above, I suggested this: 66 | ```clj 67 | (defn clock 68 | [] 69 | [:div.example-clock 70 | {:style {:color @(rf/subscribe [:time-color])}} 71 | @(rf/subscribe [:time-str])]) 72 | ``` 73 | 74 | But that may offend your aesthetics. Too much noise with those `@`? 75 | 76 | To clean this up, we can define a new `listen` function: 77 | ```clj 78 | (defn listen 79 | [query-v] 80 | @(rf/subscribe query-v)) 81 | ``` 82 | 83 | And then rewrite: 84 | ```clj 85 | (defn clock 86 | [] 87 | [:div.example-clock 88 | {:style {:color (listen [:time-color])}} 89 | (listen [:time-str])]) 90 | ``` 91 | So, at the cost of writing your own function, `listen`, the code is now less noisy 92 | AND there's less chance of us forgetting an `@` (which can lead to odd problems). 93 | 94 | ### Say It Again 95 | 96 | So, if, in code review, you saw this view function: 97 | ```clj 98 | (defn show-items 99 | [] 100 | (let [sorted-items (sort @(subscribe [:items]))] 101 | (into [:div] (for [i sorted-items] [item-view i])))) 102 | ``` 103 | What would you (supportively) object to? 104 | 105 | That `sort`, right? Computation in the view. Instead, we want the right data 106 | delivered to the view - its job is to simply make `hiccup`. 107 | 108 | The solution is to create a subscription that delivers items already sorted. 109 | ```clj 110 | (reg-sub 111 | :sorted-items 112 | (fn [_ _] (subscribe [:items])) 113 | (fn [items _] 114 | (sort items)) 115 | ``` 116 | 117 | Now, in this case the computation is a bit trivial, but the moment it is 118 | a little tricky, you'll want to test it. So separating it out from the 119 | view will make that easier. 120 | 121 | To make it testable, you may structure like this: 122 | ```clj 123 | (defn item-sorter 124 | [items _] 125 | (sort items)) 126 | 127 | (reg-sub 128 | :sorted-items 129 | (fn [_ _] (subscribe [:items])) 130 | item-sorter) 131 | ``` 132 | 133 | Now it is easy to test `item-sorter` independently. 134 | 135 | ### And There's Another Benefit 136 | 137 | re-frame de-duplicates signal graph nodes. 138 | 139 | If, for example, two views wanted to `(subscribe [:sorted-items])` only the one node 140 | (in the signal graph) would be created. Only one node would be doing that 141 | potentially expensive sorting operation (when items changed) and values from 142 | it would be flowing through to both views. 143 | 144 | That sort of efficiency can't happen if this views themselves are doing the `sort`. 145 | 146 | 147 | ### de-duplication 148 | 149 | As I described above, two, or more, concurrent subscriptions for the same query will source 150 | reactive updates from the one executing handler - from the one node in the signal graph. 151 | 152 | How do we know if two subscriptions are "the same"? Answer: two subscriptions 153 | are the same if their query vectors test `=` to each other. 154 | 155 | So, these two subscriptions are *not* "the same": `[:some-event 42]` `[:some-event "blah"]`. Even 156 | though they involve the same event id, `:some-event`, the query vectors do not test `=`. 157 | 158 | This feature shakes out nicely because re-frame has a data oriented design. 159 | 160 | ### A Final FAQ 161 | 162 | The following issue comes up a bit. 163 | 164 | You will end up with a bunch of level 1 `reg-sub` which 165 | look the same (they directly extract a path within `app-db`): 166 | ```clj 167 | (reg-sub 168 | :a 169 | (fn [db _] 170 | (:a db))) 171 | ``` 172 | 173 | ```clj 174 | (reg-sub 175 | :b 176 | (fn [db _] 177 | (-> db :top :b))) 178 | ``` 179 | 180 | Now, you think and design abstractly for a living, and that repetition will feel uncomfortable. It will 181 | call to you like a Siren: "refaaaaactoooor meeeee". "Maaaake it DRYYYY". 182 | So here's my tip: tie yourself to the mast and sail on. That repetition is good. It is serving a purpose. 183 | Just sail on. 184 | 185 | The WORST thing you can do is to flex your magnificent abstraction muscles 186 | and create something like this: 187 | ```clj 188 | (reg-sub 189 | :extract-any-path 190 | (fn [db path] 191 | (get-in db path)) 192 | ``` 193 | 194 | "Genius!", you think to yourself. "Now I only need one direct `reg-sub` and I supply a path to it. 195 | A read-only cursor of sorts. Look at the code I can delete." 196 | 197 | Neat and minimal it most certainly is, yes, but genius it isn't. You are now asking the 198 | code USING the subscription to provide the path. You have traded some innocuous 199 | repetition for longer term fragility, and that's not a good trade. 200 | 201 | What fragility? Well, the view which subscribes using, say, `(subscribe [:extract-any-path [:a]])` 202 | now "knows" about (depends on) the structure within `app-db`. 203 | 204 | What happens when you inevitably restructure `app-db` and put that `:a` path under 205 | another high level branch of `app-db`? You will have to run around all the views, 206 | looking for the paths supplied, knowing which to alter and which to leave alone. 207 | Fragile. 208 | 209 | We want our views to declaratively ask for data, but they should have 210 | no idea where it comes from. It is the job of a subscription to know where data comes from. 211 | 212 | Remember our rule at the top: **keep views as simple as possible**. 213 | Don't give them knowledge or tasks outside their remit. 214 | 215 | 216 | *** 217 | 218 | Previous: [Infographic](SubscriptionInfographic.md)       219 | Up: [Index](README.md)       220 | Next: [Flow Mechanics](SubscriptionFlow.md)        221 | 222 | 223 | 224 | 225 | 226 | -------------------------------------------------------------------------------- /docs/Talking-To-Servers.md: -------------------------------------------------------------------------------- 1 | 2 | ## Talking To Servers 3 | 4 | This page describes how a re-frame app might "talk" to a backend HTTP server. 5 | 6 | We'll assume there's a json-returning server endpoint 7 | at "http://json.my-endpoint.com/blah". We want to GET from that 8 | endpoint and put a processed version of the returned json into `app-db`. 9 | 10 | ## Triggering The Request 11 | 12 | The user often does something to trigger the process. 13 | 14 | Here's a button which the user could click: 15 | ```clj 16 | (defn request-it-button 17 | [] 18 | [:div {:class "button-class" 19 | :on-click #(dispatch [:request-it])} ;; get data from the server !! 20 | "I want it, now!"]) 21 | ``` 22 | 23 | Notice the `on-click` handler - it `dispatch`es the event `[:request-it]`. 24 | 25 | ## The Event Handler 26 | 27 | That `:request-it` event will need to be "handled", which means an event handler must be registered for it. 28 | 29 | We want this handler to: 30 | 1. Initiate the HTTP GET 31 | 2. Update a flag in `app-db` which will trigger a modal "Loading ..." message for the user to see 32 | 33 | We're going to create two versions of this event handler. First, we'll create a 34 | problematic version of the event handler and then, realising our sins, we'll write 35 | a second version which is a soaring paragon of virtue. Both versions 36 | will teach us something. 37 | 38 | 39 | ### Version 1 40 | 41 | We're going to use the [cljs-ajax library](https://github.com/JulianBirch/cljs-ajax) as the HTTP workhorse. 42 | 43 | Here's the event handler: 44 | ```clj 45 | (ns my.app.events ;; <1> 46 | (:require [ajax.core :refer [GET]] 47 | [re-frame.core :refer [reg-event-db])) 48 | 49 | (reg-event-db ;; <-- register an event handler 50 | :request-it ;; <-- the event id 51 | (fn ;; <-- the handler function 52 | [db _] 53 | 54 | ;; kick off the GET, making sure to supply a callback for success and failure 55 | (GET 56 | "http://json.my-endpoint.com/blah" 57 | {:handler #(dispatch [:process-response %1]) ;; <2> further dispatch !! 58 | :error-handler #(dispatch [:bad-response %1])}) ;; <2> further dispatch !! 59 | 60 | ;; update a flag in `app-db` ... presumably to cause a "Loading..." UI 61 | (assoc db :loading? true))) ;; <3> return an updated db 62 | ``` 63 | 64 | Further Notes: 65 | 1. Event handlers are normally put into an `events.cljs` namespace 66 | 2. Notice that the GET callbacks issue a further `dispatch`. Such callbacks 67 | should never attempt to close over `db` themselves, or make 68 | any changes to it because, by the time these callbacks happen, the value 69 | in `app-db` may have changed. Whereas, if they `dispatch`, then the event 70 | handlers looking after the event they dispatch will be given the latest copy of the db. 71 | 3. event handlers registered using `reg-event-db` must return a new value for 72 | `app-db`. In our case, we set a flag which will presumably cause a "Loading ..." 73 | UI to show. 74 | 75 | ### Successful GET 76 | 77 | As we noted above, the on-success handler itself is just 78 | `(dispatch [:process-response RESPONSE])`. So we'll need to register a handler 79 | for this event too. 80 | 81 | Like this: 82 | ```clj 83 | (reg-event-db 84 | :process-response 85 | (fn 86 | [db [_ response]] ;; destructure the response from the event vector 87 | (-> db 88 | (assoc :loading? false) ;; take away that "Loading ..." UI 89 | (assoc :data (js->clj response)))) ;; fairly lame processing 90 | ``` 91 | 92 | A normal handler would have more complex processing of the response. But we're 93 | just sketching here, so we've left it easy. 94 | 95 | There'd also need to be a handler for the `:bad-response` event too. Left as an exercise. 96 | 97 | ### Problems In Paradise? 98 | 99 | This approach will work, and it is useful to take time to understand why it 100 | would work, but it has a problem: the event handler isn't pure. 101 | 102 | That `GET` is a side effect, and side effecting functions are like a 103 | well salted paper cut. We try hard to avoid them. 104 | 105 | ### Version 2 106 | 107 | The better solution is, of course, to use an effectful handler. This 108 | is explained in detail in the previous tutorials: [Effectful Handlers](EffectfulHandlers.md) 109 | and [Effects](Effects.md). 110 | 111 | In the 2nd version, we use the alternative registration function, `reg-event-fx` , and we'll use an 112 | "Effect Handler" supplied by this library 113 | [https://github.com/Day8/re-frame-http-fx](https://github.com/Day8/re-frame-http-fx). 114 | You may soon feel confident enough to write your own. 115 | 116 | Here's our rewrite: 117 | 118 | ```clj 119 | (ns my.app.events 120 | (:require 121 | [ajax.core :as ajax] 122 | [day8.re-frame.http-fx] 123 | [re-frame.core :refer [reg-event-fx])) 124 | 125 | (reg-event-fx ;; <-- note the `-fx` extension 126 | :request-it ;; <-- the event id 127 | (fn ;; <-- the handler function 128 | [{db :db} _] ;; <-- 1st argument is coeffect, from which we extract db 129 | 130 | ;; we return a map of (side) effects 131 | {:http-xhrio {:method :get 132 | :uri "http://json.my-endpoint.com/blah" 133 | :format (ajax/json-request-format) 134 | :response-format (ajax/json-response-format {:keywords? true}) 135 | :on-success [:process-response] 136 | :on-failure [:bad-response]} 137 | :db (assoc db :loading? true)})) 138 | ``` 139 | 140 | Notes: 141 | 1. Our event handler "describes" side effects, it does not "do" side effects 142 | 2. The event handler we wrote for `:process-response` stays as it was 143 | 144 | 145 | 146 | *** 147 | 148 | Previous: [Loading Initial Data](Loading-Initial-Data.md)       149 | Up: [Index](README.md)       150 | Next: [Subscribing to External Data](Subscribing-To-External-Data.md) 151 | 152 | 153 | 154 | 155 | 156 | 157 | -------------------------------------------------------------------------------- /docs/Testing.md: -------------------------------------------------------------------------------- 1 | ## Testing 2 | 3 | This is an introductory, simple exploration of testing re-frame apps. If you want some more help see [re-frame-test](https://github.com/Day8/re-frame-test) 4 | 5 | With a re-frame app, there's principally three things to test: 6 | 1. Event handlers 7 | 2. Subscription handlers 8 | 3. View functions 9 | 10 | ## Event Handlers - Part 1 11 | 12 | Event Handlers are pure functions and consequently easy to test. 13 | 14 | First, create an event handler like this: 15 | ```clj 16 | (defn my-db-handler 17 | [db v] 18 | ... return a modified version of db) 19 | ``` 20 | 21 | Then, register it in a separate step: 22 | ```clj 23 | (re-frame.core/reg-event-db 24 | :some-id 25 | [some-interceptors] 26 | my-db-handler) 27 | ``` 28 | 29 | With this setup, `my-db-handler` is available for direct testing. 30 | 31 | Your unittests will pass in certain values for `db` and `v`, 32 | and then ensure it returns the right (modified version of) `db`. 33 | 34 | ## Subscription Handlers 35 | 36 | Here's a subscription handler from [the todomvc example](https://github.com/Day8/re-frame/blob/master/examples/todomvc/src/todomvc/subs.cljs): 37 | 38 | ```clj 39 | (reg-sub 40 | :visible-todos 41 | 42 | ;; signal function 43 | (fn [query-v _] 44 | [(subscribe [:todos]) 45 | (subscribe [:showing])]) 46 | 47 | ;; computation function 48 | (fn [[todos showing] _] ;; that 1st parameter is a 2-vector of values 49 | (let [filter-fn (case showing 50 | :active (complement :done) 51 | :done :done 52 | :all identity)] 53 | (filter filter-fn todos)))) 54 | ``` 55 | 56 | How do we test this? 57 | 58 | We could split the computation function from its registration, like this: 59 | ```clj 60 | (defn visible-todos 61 | [[todos showing] _] 62 | 63 | (let [filter-fn (case showing 64 | :active (complement :done) 65 | :done :done 66 | :all identity)] 67 | (filter filter-fn todos))) 68 | 69 | (reg-sub 70 | :visible-todos 71 | (fn [query-v _] 72 | [(subscribe [:todos]) 73 | (subscribe [:showing])]) 74 | visible-todos) ;; <--- computation function used here 75 | ``` 76 | 77 | That makes `visible-todos` available for direct unit testing. 78 | 79 | ## View Functions - Part 1 80 | 81 | Components/views are slightly more tricky. There's a few options. 82 | 83 | First, I have to admit an ugly secret. I don't tend to write tests for my views. 84 | Hey, don't give me that disproving frown! I have my reasons. 85 | 86 | Remember that every line of code you write is a liability. So tests have to earn 87 | their keep - they have to deliver a good cost / benefit ratio. And, in my experience 88 | with the re-frame architecture, the Reagent view components tend to be an unlikely 89 | source of bugs. There's just not much logic in them for me to get wrong. 90 | 91 | Okay, fine, don't believe me, then!! 92 | 93 | Here's how, theoretically, I'd write tests if I wasn't me ... 94 | 95 | If a Components is a [Form-1](https://github.com/Day8/re-frame/wiki/Creating-Reagent-Components#form-1-a-simple-function) 96 | structure, then it is fairly easy to test. 97 | 98 | A trivial example: 99 | ```clj 100 | (defn greet 101 | [name] 102 | [:div "Hello " name]) 103 | 104 | (greet "Wiki") 105 | ;;=> [:div "Hello " "Wiki"] 106 | ``` 107 | 108 | So, here, testing involves passing values into the function and checking the data structure returned for correctness. 109 | 110 | What's returned is hiccup, of course. So how do you test hiccup for correctness? 111 | 112 | hiccup is just a clojure data structure - vectors containing keywords, and maps, and other vectors, etc. 113 | Perhaps you'd use https://github.com/nathanmarz/specter to declaratively check on the presence of certain values and structures? Or do it more manually. 114 | 115 | 116 | ## View Functions - Part 2A 117 | 118 | But what if the View Function has a subscription (via a [Form-2](https://github.com/Day8/re-frame/wiki/Creating-Reagent-Components#form-2--a-function-returning-a-function) structure)? 119 | 120 | ```clj 121 | (defn my-view 122 | [something] 123 | (let [val (subscribe [:query-id])] <-- reactive subscription 124 | [:div .... using @val in here]))) 125 | ``` 126 | 127 | There's no immediately obvious way to test this as a lovely pure function. Because it is not pure. 128 | 129 | Of course, less pure ways are very possible. For example, a plan might be: 130 | 1. setup `app-db` with some values in the right places (for the subscription) 131 | 2. call `my-view` (with a parameter) which will return hiccup 132 | 3. check the hiccup structure for correctness. 133 | 134 | Continuing on, in a second phase you could then: 135 | 5. change the value in `app-db` (which will cause the subscription to fire) 136 | 6. call view functions again (hiccup returned). 137 | 7. check that the hiccup 138 | 139 | Which is all possible, if a little messy, and with one gotcha. After you change the 140 | value in `app-db` the subscription won't hold the new value straight away. 141 | It won't get calculated until the next animationFrame. And the next animationFrame 142 | won't happen until you hand back control to the browser. I think. Untested. 143 | Please report back here if you try. And you might also be able to use `reagent.core/flush` to force the view to be updated. 144 | 145 | ## View Functions - Part 2B 146 | 147 | Or ... instead of the not-very-pure method above, you could use `with-redefs` on `subscribe` to stub out re-frame altogether: 148 | 149 | ```clj 150 | (defn subscription-stub [x] 151 | (atom 152 | (case x 153 | [:query-id] 42))) 154 | 155 | (deftest some-test 156 | (with-redefs [re-frame/subscribe (subscription-stub)] 157 | (testing "some rendering" 158 | ..... somehow call or render the component and check the output))) 159 | ``` 160 | 161 | For more integration level testing, you can use `with-mounted-component` 162 | from the [reagent-template](https://github.com/reagent-project/reagent-template/blob/master/src/leiningen/new/reagent/test/cljs/reagent/core_test.cljs) to render the component in the browser and validate the generated DOM. 163 | 164 | ## View Functions - Part 2C 165 | 166 | Or ... you can structure in the first place for easier testing and pure functions. 167 | 168 | The trick here is to create an outer and inner component. The outer sources the data 169 | (via a subscription), and passes it onto the inner as props (parameters). 170 | 171 | As a result, the inner component, which does the testable work, is pure and 172 | easily tested. The outer is fairly trivial. 173 | 174 | To get a more concrete idea, I'll direct you to another page on this Wiki 175 | which has nothing to do with testing, but it does use this `simple-outer-subscribe-with-complicated-inner-render` 176 | pattern for a different purpose: [[Using-Stateful-JS-Components]] 177 | 178 | Note this technique could be made simple and almost invisible via the 179 | use of macros. (Contribute one if you have it). 180 | 181 | This pattern has been independently discovered by many. For example, here 182 | it is called the [Container/Component pattern](https://medium.com/@learnreact/container-components-c0e67432e005#.mb0hzgm3l). 183 | 184 | 185 | ## Summary 186 | 187 | So, we stumbled slightly at the final hurdle with Form-2 Components. But prior 188 | to this, the testing story for re-frame was as good as it gets: you are testing 189 | a bunch of simple, pure functions. No dependency injection in sight! 190 | 191 | 192 | 193 | 194 | 195 | -------------------------------------------------------------------------------- /docs/The-re-frame-logo.md: -------------------------------------------------------------------------------- 1 | ## The re-frame Logo 2 | 3 | ![logo](/images/logo/re-frame_256w.png?raw=true) 4 | 5 | ### Who 6 | 7 | Created by the mysterious, deep thinker, known only as @martinklepsch. 8 | 9 | Some say he appears on high value stamps in Germany and that he once
10 | punched a horse to the ground. Others say he loves recursion so much that,
11 | in his wallet, he carries a photograph of his wallet. 12 | 13 | All we know for sure is that he wields [Sketch.app](https://www.sketchapp.com/) like Bruce Lee
14 | wielded nunchucks. 15 | 16 | ### Genesis Theories 17 | 18 | Great, unexplained works encourage fan theories, and the re-frame
19 | logo is no exception. 20 | 21 | One noisy group thinks @martinklepsch simply wanted to
22 | `Put the 'f' back into infinity`. They have t-shirts. 23 | 24 | Another group speculates that he created the logo as a bifarious
25 | rainbow homage to Frank Lloyd Wright's masterpiece, the Guggenheim
26 | Museum. A classic case of premature abstraction and over engineering
27 | if you ask me. Their theory, not the Guggenheim. 28 | 29 | ![](/images/logo/Guggenheim.jpg) 30 | 31 | The infamous "Bad Touch" faction look at the logo and see the cljs
32 | logo mating noisily with re-frame's official architecture diagram.
33 | Attend one of their parties and you have a 50% chance of arrest. 34 | 35 | ![](/images/logo/Genesis.png) 36 | 37 | The Functional Fundamentalists, a stern bunch, see the logo as a
38 | flowing poststructuralist rebuttal of OO's vowel duplication and
39 | horizontal adjacency. Their alternative approach, FF, is apparently
40 | fine because "everyone loves a fricative". 41 | 42 | For his part, @martinklepsch has never confirmed any theory, teasing
43 | us instead with coded clues like "Will you please stop emailing me"
44 | and "Why did you say I hit a horse?". 45 | 46 | ### Assets Where? 47 | 48 | Within this repo, look in `/images/logo/` 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /docs/Using-Stateful-JS-Components.md: -------------------------------------------------------------------------------- 1 | ## Using Stateful JS Components 2 | 3 | You know what's good for you, and you know what's right. But it 4 | doesn't matter - the wickedness of the temptation is too much. 5 | 6 | The JS world is brimming with shiny component baubles: D3, 7 | Google Maps, Chosen, etc. 8 | 9 | But they are salaciously stateful and mutative. And, you, 10 | raised in a pure, functional home, with caring, immutable parents, 11 | know they are wrong. But, my, how you still yearn for the sweet 12 | thrill of that forbidden fruit. 13 | 14 | I won't tell, if you don't. But careful plans must be made ... 15 | 16 | ### The overall plan 17 | 18 | To use a stateful js component, you'll need to write two Reagent components: 19 | - an **outer component** responsible for sourcing data via a subscription or r/atom or cursor, etc. 20 | - an **inner component** responsible for wrapping and manipulating the stateful JS component via lifecycle functions. 21 | 22 | The pattern involves the outer component, which sources data, supplying this data to the inner component **via props**. 23 | 24 | ### Example Using Google Maps 25 | 26 | ```cljs 27 | (defn gmap-inner [] 28 | (let [gmap (atom nil) 29 | options (clj->js {"zoom" 9}) 30 | update (fn [comp] 31 | (let [{:keys [latitude longitude]} (reagent/props comp) 32 | latlng (js/google.maps.LatLng. latitude longitude)] 33 | (.setPosition (:marker @gmap) latlng) 34 | (.panTo (:map @gmap) latlng)))] 35 | 36 | (reagent/create-class 37 | {:reagent-render (fn [] 38 | [:div 39 | [:h4 "Map"] 40 | [:div#map-canvas {:style {:height "400px"}}]]) 41 | 42 | :component-did-mount (fn [comp] 43 | (let [canvas (.getElementById js/document "map-canvas") 44 | gm (js/google.maps.Map. canvas options) 45 | marker (js/google.maps.Marker. (clj->js {:map gm :title "Drone"}))] 46 | (reset! gmap {:map gm :marker marker})) 47 | (update comp)) 48 | 49 | :component-did-update update 50 | :display-name "gmap-inner"}))) 51 | 52 | 53 | 54 | (defn gmap-outer [] 55 | (let [pos (subscribe [:current-position])] ;; obtain the data 56 | (fn [] 57 | [gmap-inner @pos]))) 58 | ``` 59 | 60 | 61 | Notes: 62 | - `gmap-outer` obtains data via a subscription. It is quite simple - trivial almost. 63 | - it then passes this data __as a prop__ to `gmap-inner`. This inner component has the job of wrapping/managing the stateful js component (Gmap in our case above) 64 | - when the data (delivered by the subscription) to the outer layer changes, the inner layer, `gmap-inner`, will be given a new prop - `@pos` in the case above. 65 | - when the inner component is given new props, its entire set of lifecycle functions will be engaged. 66 | - the renderer for the inner layer ALWAYS renders the same, minimal container hiccup for the component. Even though the `props` have changed, the same hiccup is output. So it will appear to React as if nothing changes from one render to the next. No work to be done. React/Reagent will leave the DOM untouched. 67 | - but this inner component has other lifecycle functions and this is where the real work is done. 68 | - for example, after the renderer is called (which ignores its props), `component-did-update` will be called. In this function, we don't ignore the props, and we use them to update/mutate the stateful JS component. 69 | - the props passed (in this case `@pos`) in must be a map, otherwise `(reagent/props comp)` will return nil. 70 | 71 | ### Pattern Discovery 72 | 73 | This pattern has been independently discovered by many. To my knowledge, 74 | [this description of the Container/Component pattern](https://medium.com/@learnreact/container-components-c0e67432e005#.3ic1uipvu) 75 | is the first time it was written up. 76 | 77 | ### Code Credit 78 | 79 | The example gmaps code above was developed by @jhchabran in this gist: 80 | https://gist.github.com/jhchabran/e09883c3bc1b703a224d#file-2_google_map-cljs 81 | 82 | ### D3 Examples 83 | 84 | D3 (from @zachcp): 85 | - Blog Post: http://zachcp.org/blog/2015/reagent-d3/ 86 | - Code: https://github.com/zachcp/simplecomponent 87 | - Example: http://zachcp.github.io/simplecomponent/ 88 | 89 | A different take on using D3: 90 | https://gadfly361.github.io/gadfly-blog/2016-10-22-d3-in-reagent.html 91 | 92 | ### Advanced Lifecycle Methods 93 | 94 | If you mess around with lifecycle methods, you'll probably want to read Martin's explanations: 95 | https://www.martinklepsch.org/posts/props-children-and-component-lifecycle-in-reagent.html 96 | 97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /docs/re-frankenstein/about-effects.md: -------------------------------------------------------------------------------- 1 | # re-frankenstein - about effects 2 | 3 | Backward compatibility with re-frame is taken really seriously in this fork. The 4 | idea was to be able to provide new features without changing too much the way 5 | re-frame is used, particularly in terms of registering handlers, effects and 6 | coeffects. 7 | 8 | That is why in the first implementation of re-frankenstein, the `do-fx` 9 | interceptor that was swapped in place of its original counterpart (on 10 | `(frank/create)`) was strictly identical to it except for the use of a local 11 | handler registry (instead of the global registry). 12 | 13 | Sadly, the result of this is that, apart from the `:db` effect, no other effect 14 | could cause affect the local db! So we are going to move just a little bit away 15 | from the original `do-fx` implementation. 16 | 17 | 18 | ## What re-frankenstein changes in 0.0.3 19 | 20 | The `do-fx` interceptor injected at the creation of a `frank` is now slightly 21 | different from in the way it invokes the effect handlers: 22 | 23 | ``` 24 | ;; The original way 25 | (effect-fn value) 26 | 27 | ;; The re-frankenstein way 28 | (effect-fn value {:dispatch! #(dispatch! frank %) 29 | :dispatch-sync! #(dispatch-sync! frank %)}) 30 | ``` 31 | 32 | The new `do-fx` interceptor provides the effect handler a dispatch function they 33 | may call if they need to locally dispatch further. As the db and the handler 34 | registry are local when using a `frank` passing a dispatch function to the 35 | effects handler is in fact the only way for them to modify the local db of the 36 | `frank` instance. 37 | 38 | That means that when you registering an effect that you want to use locally on a 39 | `frank`, you will need to pass a function of 2 arguments: 40 | 41 | ``` 42 | (reg-fx ::some-effect 43 | (fn [value {dispatch! :dispatch!}] 44 | ;; Do the side effecting here and dispatch if you need to modify state. 45 | )) 46 | ``` 47 | 48 | -------------------------------------------------------------------------------- /examples/simple/README.md: -------------------------------------------------------------------------------- 1 | # A Simple App 2 | 3 | This tiny application is meant to provide a quick start of the basics of re-frame. 4 | 5 | A detailed source code walk-through is provided in the docs: 6 | https://github.com/Day8/re-frame/blob/master/docs/CodeWalkthrough.md 7 | 8 | All the code is in one namespace: `/src/simpleexample/core.cljs` 9 | 10 | ### Run It And Change It 11 | 12 | Steps: 13 | 14 | 1. Check out the re-frame repo 15 | 2. Get a command line 16 | 3. `cd` to the root of this sub project (where this README exists) 17 | 4. run "`lein do clean, figwheel`" to compile the app and start up figwheel hot-reloading, 18 | 5. open `http://localhost:3449/example.html` to see the app 19 | 20 | While step 4 is running, any changes you make to the ClojureScript 21 | source files (in `src`) will be re-compiled and reflected in the running 22 | page immediately. 23 | 24 | ### Production Version 25 | 26 | Run "`lein do clean, with-profile prod compile`" to compile an optimised 27 | version, and then open `resources/public/example.html` in a browser. 28 | -------------------------------------------------------------------------------- /examples/simple/project.clj: -------------------------------------------------------------------------------- 1 | (defproject simple "0.9.0" 2 | :dependencies [[org.clojure/clojure "1.8.0"] 3 | [org.clojure/clojurescript "1.9.227"] 4 | [cljsjs/react "15.4.2-0"] 5 | [cljsjs/react-dom "15.4.2-0"] 6 | [reagent "0.6.0-rc"] 7 | [org.martinklepsch/derivatives "0.2.0"] 8 | [rum "0.10.8"] 9 | 10 | [com.chpill.re-frankenstein "0.0.1-SNAPSHOT"]] 11 | 12 | :plugins [[lein-cljsbuild "1.1.3"] 13 | [lein-figwheel "0.5.4-7"]] 14 | 15 | :hooks [leiningen.cljsbuild] 16 | 17 | :profiles {:dev {:cljsbuild 18 | {:builds {:client {:figwheel {:on-jsload "simple.core/run"} 19 | :compiler {:main "simple.core" 20 | :asset-path "js" 21 | :optimizations :none 22 | :source-map true 23 | :source-map-timestamp true}}}}} 24 | 25 | :prod {:cljsbuild 26 | {:builds {:client {:compiler {:optimizations :advanced 27 | :elide-asserts true 28 | :pretty-print false}}}}}} 29 | 30 | :figwheel {:repl false} 31 | 32 | :clean-targets ^{:protect false} ["resources/public/js"] 33 | 34 | :cljsbuild {:builds {:client {:source-paths ["src"] 35 | :compiler {:output-dir "resources/public/js" 36 | :output-to "resources/public/js/client.js"}}}}) 37 | -------------------------------------------------------------------------------- /examples/simple/resources/public/example.css: -------------------------------------------------------------------------------- 1 | 2 | div, h1, input { 3 | font-family: HelveticaNeue, Helvetica; 4 | color: #777; 5 | } 6 | 7 | .example-clock { 8 | font-size: 128px; 9 | line-height: 1.2em; 10 | font-family: HelveticaNeue-UltraLight, Helvetica; 11 | } 12 | 13 | @media (max-width: 768px) { 14 | .example-clock { 15 | font-size: 64px; 16 | } 17 | } 18 | 19 | .color-input, .color-input input { 20 | font-size: 24px; 21 | line-height: 1.5em; 22 | } 23 | -------------------------------------------------------------------------------- /examples/simple/resources/public/example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Example 6 | 7 | 8 | 9 |
10 |

Reagent example app – see README.md

11 |
12 |
13 |
14 |

Re-Frankenstein app should go here

15 |
16 | 17 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /examples/simple/src/simple/core.cljs: -------------------------------------------------------------------------------- 1 | (ns simple.core 2 | (:require [reagent.core :as reagent] 3 | [rum.core :as rum] 4 | [re-frame.rum :as re-rum] 5 | [re-frame.frank :as frank] 6 | [org.martinklepsch.derivatives :as derivatives] 7 | [re-frame.core :as rf])) 8 | 9 | ;; A detailed walk-through of this source code is provied in the docs: 10 | ;; https://github.com/Day8/re-frame/blob/master/docs/CodeWalkthrough.md 11 | 12 | ;; -- Domino 1 - Event Dispatch ----------------------------------------------- 13 | 14 | (defn dispatch-timer-event 15 | [] 16 | (let [now (js/Date.)] 17 | (rf/dispatch [:timer now]))) ;; <-- dispatch used 18 | 19 | ;; Call the dispatching function every second. 20 | ;; `defonce` is like `def` but it ensures only instance is ever 21 | ;; created in the face of figwheel hot-reloading of this file. 22 | (defonce do-timer (js/setInterval dispatch-timer-event 1000)) 23 | 24 | 25 | ;; -- Domino 1 bis - See the end of this file! 26 | 27 | 28 | ;; -- Domino 2 - Event Handlers ----------------------------------------------- 29 | 30 | (rf/reg-event-db ;; sets up initial application state 31 | :initialize ;; usage: (dispatch [:initialize]) 32 | (fn [_ _] ;; the two parameters are not important here, so use _ 33 | {:time (js/Date.) ;; What it returns becomes the new application state 34 | :time-color "#f88"})) ;; so the application state will initially be a map with two keys 35 | 36 | 37 | (rf/reg-event-db ;; usage: (dispatch [:time-color-change 34562]) 38 | :time-color-change ;; dispatched when the user enters a new colour into the UI text field 39 | (fn [db [_ new-color-value]] ;; -db event handlers given 2 parameters: current application state and event (a vector) 40 | (assoc db :time-color new-color-value))) ;; compute and return the new application state 41 | 42 | 43 | (rf/reg-event-db ;; usage: (dispatch [:timer a-js-Date]) 44 | :timer ;; every second an event of this kind will be dispatched 45 | (fn [db [_ new-time]] ;; note how the 2nd parameter is destructured to obtain the data value 46 | (assoc db :time new-time))) ;; compute and return the new application state 47 | 48 | 49 | ;; Note that there is no need for a Domino 2 bis! 50 | 51 | 52 | ;; -- Domino 4 - Query ------------------------------------------------------- 53 | 54 | (rf/reg-sub 55 | :time 56 | (fn [db _] ;; db is current app state. 2nd unused param is query vector 57 | (-> db 58 | :time))) 59 | 60 | (rf/reg-sub 61 | :time-color 62 | (fn [db _] 63 | (:time-color db))) 64 | 65 | 66 | ;; -- Domino 4 bis - Derivatives ------------------------------------------------------- 67 | 68 | (defn make-derivative-specs [base] 69 | {:base [[] base] 70 | :time [[:base] :time] 71 | :time-color [[:base] :time-color]}) 72 | 73 | 74 | ;; -- Domino 5 - View Functions ---------------------------------------------- 75 | 76 | (defn clock 77 | [] 78 | [:div.example-clock 79 | {:style {:color @(rf/subscribe [:time-color])}} 80 | (-> @(rf/subscribe [:time]) 81 | .toTimeString 82 | (clojure.string/split " ") 83 | first)]) 84 | 85 | (defn color-input 86 | [] 87 | [:div.color-input 88 | "Time color: " 89 | [:input {:type "text" 90 | :value @(rf/subscribe [:time-color]) 91 | :on-change #(rf/dispatch [:time-color-change (-> % .-target .-value)])}]]) ;; <--- 92 | 93 | (defn ui 94 | [] 95 | [:div 96 | [:h1 "Hello world, it is now"] 97 | [clock] 98 | [color-input]]) 99 | 100 | 101 | ;; -- Domino 5 bis - Rum View Functions ---------------------------------------------- 102 | ;; Note that there are no globals here. The dispatch functions and the 103 | ;; subscription to derived values go through the react context. 104 | 105 | (rum/defcs rum-clock 106 | < rum/reactive 107 | (derivatives/drv :time :time-color) 108 | [rum-state] 109 | [:div.example-clock 110 | {:style {:color (derivatives/react rum-state :time-color)}} 111 | (-> (derivatives/react rum-state :time) 112 | .toTimeString 113 | (clojure.string/split " ") 114 | first)]) 115 | 116 | (rum/defcs rum-color-input 117 | < rum/reactive 118 | (derivatives/drv :time-color) 119 | re-rum/with-frank 120 | re-rum/drv+frank-ctx 121 | 122 | [{dispatch! :frank/dispatch! :as rum-state}] 123 | [:div.color-input 124 | "Time color: " 125 | [:input {:type "text" 126 | :value (derivatives/react rum-state :time-color) 127 | :on-change #(dispatch! [:time-color-change (-> % .-target .-value)])}]]) 128 | 129 | (rum/defc rum-ui [] 130 | [:div 131 | [:h1 "Hello Victor, it is now"] 132 | (rum-clock) 133 | (rum-color-input)]) 134 | 135 | (rum/defc rum-root 136 | "This component does not render anything by itself, it just calls another 137 | view. It does however take inputs and injects them into the local react context" 138 | < (re-rum/inject-frank-into-context first) 139 | (derivatives/rum-derivatives* second) 140 | re-rum/drv+frank-child-ctx 141 | [frank derivative-specs] 142 | (rum-ui)) 143 | 144 | 145 | ;; -- Entry Point ------------------------------------------------------------- 146 | 147 | (defn ^:export run 148 | [] 149 | ;; Mount the original app 150 | (rf/dispatch-sync [:initialize]) ;; puts a value into application state 151 | (reagent/render [ui] ;; mount the application's ui into '
' 152 | (js/document.getElementById "app")) 153 | 154 | ;; Create and mount a new app using `re-frame.frank` 155 | (let [frank (frank/create)] 156 | (frank/dispatch-sync! frank [:initialize]) 157 | (frank/dispatch-sync! frank [:time-color-change "#8d8"]) 158 | 159 | ;; Domino 1 bis must be set up here, because we need the frank instance to dispatch 160 | (let [scoped-dispatch-timer-event 161 | (fn [] (let [now (js/Date.)] 162 | ;; We are going to add an hour to make very explicit we do 163 | ;; not display the global state 164 | (frank/dispatch! frank [:timer now])))] 165 | 166 | ;; DO NOT PUT `defonce` here. 167 | ;; You need to to it with vanilla re-frame because there is a `defonce app-db`. 168 | ;; Here we re-create the frank instance on every reload of the file so no problem. 169 | (js/setInterval scoped-dispatch-timer-event 1000)) 170 | 171 | (rum/mount (rum-root frank 172 | (make-derivative-specs frank)) 173 | (js/document.getElementById "frankenstein")))) 174 | 175 | -------------------------------------------------------------------------------- /examples/todomvc/.gitignore: -------------------------------------------------------------------------------- 1 | resources/public/js 2 | -------------------------------------------------------------------------------- /examples/todomvc/README.md: -------------------------------------------------------------------------------- 1 | # TodoMVC done with re-frame 2 | 3 | A [re-frame](https://github.com/Day8/re-frame) implementation of [TodoMVC](http://todomvc.com/). 4 | 5 | 6 | ## Setup And Run 7 | 8 | 1. Install [Leiningen](http://leiningen.org/) (plus Java). 9 | 10 | 2. Get the re-frame repo 11 | ``` 12 | git clone https://github.com/Day8/re-frame.git 13 | ``` 14 | 15 | 3. cd to the right example directory 16 | ``` 17 | cd re-frame/examples/todomvc 18 | ``` 19 | 20 | 4. Clean build 21 | ``` 22 | lein do clean, figwheel 23 | ``` 24 | 25 | 5. Run 26 | You'll have to wait for step 4 to do its compile, but then: 27 | ``` 28 | open http://localhost:3450 29 | ``` 30 | 31 | 32 | ## Compile an optimised version 33 | 34 | 1. Compile 35 | ``` 36 | lein do clean, with-profile prod compile 37 | ``` 38 | 39 | 2. Open the following in your browser 40 | ``` 41 | resources/public/index.html 42 | ``` 43 | 44 | 45 | ## Exploring The Code 46 | 47 | From the re-frame readme: 48 | ``` 49 | To build a re-frame app, you: 50 | - design your app's data structure (data layer) 51 | - write and register subscription functions (query layer) 52 | - write Reagent component functions (view layer) 53 | - write and register event handler functions (control layer and/or state transition layer) 54 | ``` 55 | 56 | In `src`, there's a matching set of files (each small): 57 | ``` 58 | src 59 | ├── core.cljs <--- entry point, plus history 60 | ├── db.cljs <--- data related (data layer) 61 | ├── subs.cljs <--- subscription handlers (query layer) 62 | ├── views.cljs <--- reagent components (view layer) 63 | └── events.cljs <--- event handlers (control/update layer) 64 | ``` 65 | 66 | ## Notes 67 | 68 | Various: 69 | - The [official reagent example](https://github.com/reagent-project/reagent/tree/master/examples/todomvc) 70 | - Look at the [re-frame Wiki](https://github.com/Day8/re-frame/wiki) 71 | -------------------------------------------------------------------------------- /examples/todomvc/project.clj: -------------------------------------------------------------------------------- 1 | (defproject todomvc-re-frame "0.9.0" 2 | :dependencies [[org.clojure/clojure "1.8.0"] 3 | [org.clojure/clojurescript "1.9.89"] 4 | [reagent "0.6.0-rc"] 5 | [cljsjs/react "15.4.2-0"] 6 | [cljsjs/react-dom "15.4.2-0"] 7 | [org.martinklepsch/derivatives "0.2.0"] 8 | [rum "0.10.8"] 9 | [binaryage/devtools "0.8.1"] 10 | [secretary "1.2.3"] 11 | 12 | [com.chpill.re-frankenstein "0.0.1-SNAPSHOT"]] 13 | 14 | :plugins [[lein-cljsbuild "1.1.4"] 15 | [lein-figwheel "0.5.6"]] 16 | 17 | :hooks [leiningen.cljsbuild] 18 | 19 | :profiles {:dev {:cljsbuild 20 | {:builds {:client {:compiler {:asset-path "js" 21 | :optimizations :none 22 | :source-map true 23 | :source-map-timestamp true 24 | :main "todomvc.core"} 25 | :figwheel {:on-jsload "todomvc.core/main"}}}}} 26 | 27 | :prod {:cljsbuild 28 | {:builds {:client {:compiler {:optimizations :advanced 29 | :elide-asserts true 30 | :pretty-print false}}}}}} 31 | 32 | :figwheel {:server-port 3450 33 | :repl true} 34 | 35 | 36 | :clean-targets ^{:protect false} ["resources/public/js" "target"] 37 | 38 | :cljsbuild {:builds {:client {:source-paths ["src" "../../src"] 39 | :compiler {:output-dir "resources/public/js" 40 | :output-to "resources/public/js/client.js"}}}}) 41 | -------------------------------------------------------------------------------- /examples/todomvc/resources/public/frank-todos.css: -------------------------------------------------------------------------------- 1 | .todoapp { 2 | background: #fff; 3 | margin: 130px 0 40px 0; 4 | position: relative; 5 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 6 | 0 25px 50px 0 rgba(0, 0, 0, 0.1); 7 | } 8 | 9 | .todoapp input::-webkit-input-placeholder { 10 | font-style: italic; 11 | font-weight: 300; 12 | color: #e6e6e6; 13 | } 14 | 15 | .todoapp input::-moz-placeholder { 16 | font-style: italic; 17 | font-weight: 300; 18 | color: #e6e6e6; 19 | } 20 | 21 | .todoapp input::input-placeholder { 22 | font-style: italic; 23 | font-weight: 300; 24 | color: #e6e6e6; 25 | } 26 | 27 | .todoapp h1 { 28 | position: absolute; 29 | top: -155px; 30 | width: 100%; 31 | font-size: 100px; 32 | font-weight: 100; 33 | text-align: center; 34 | color: rgba(175, 47, 47, 0.15); 35 | -webkit-text-rendering: optimizeLegibility; 36 | -moz-text-rendering: optimizeLegibility; 37 | text-rendering: optimizeLegibility; 38 | } 39 | 40 | .new-todo, 41 | .edit { 42 | position: relative; 43 | margin: 0; 44 | width: 100%; 45 | font-size: 24px; 46 | font-family: inherit; 47 | font-weight: inherit; 48 | line-height: 1.4em; 49 | border: 0; 50 | outline: none; 51 | color: inherit; 52 | padding: 6px; 53 | border: 1px solid #999; 54 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); 55 | box-sizing: border-box; 56 | -webkit-font-smoothing: antialiased; 57 | -moz-font-smoothing: antialiased; 58 | font-smoothing: antialiased; 59 | } 60 | 61 | .new-todo { 62 | padding: 16px 16px 16px 60px; 63 | border: none; 64 | background: rgba(0, 0, 0, 0.003); 65 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); 66 | } 67 | 68 | .main { 69 | position: relative; 70 | z-index: 2; 71 | border-top: 1px solid #e6e6e6; 72 | } 73 | 74 | .toggle-all { 75 | position: absolute; 76 | top: -55px; 77 | left: -12px; 78 | width: 60px; 79 | height: 34px; 80 | text-align: center; 81 | border: none; /* Mobile Safari */ 82 | } 83 | 84 | .toggle-all:before { 85 | content: '❯'; 86 | font-size: 22px; 87 | color: #e6e6e6; 88 | padding: 10px 27px 10px 27px; 89 | } 90 | 91 | .toggle-all:checked:before { 92 | color: #737373; 93 | } 94 | 95 | .todo-list { 96 | margin: 0; 97 | padding: 0; 98 | list-style: none; 99 | } 100 | 101 | .todo-list li { 102 | position: relative; 103 | font-size: 24px; 104 | border-bottom: 1px solid #ededed; 105 | } 106 | 107 | 108 | .todo-list li:last-child { 109 | border-bottom: none; 110 | } 111 | 112 | .todo-list li.editing { 113 | border-bottom: none; 114 | padding: 0; 115 | } 116 | 117 | .todo-list li.editing .edit { 118 | display: block; 119 | width: 506px; 120 | padding: 13px 17px 12px 17px; 121 | margin: 0 0 0 43px; 122 | } 123 | 124 | .todo-list li.editing .view { 125 | display: none; 126 | } 127 | 128 | .todo-list li .toggle { 129 | text-align: center; 130 | width: 40px; 131 | /* auto, since non-WebKit browsers doesn't support input styling */ 132 | height: auto; 133 | position: absolute; 134 | top: 0; 135 | bottom: 0; 136 | margin: auto 0; 137 | border: none; /* Mobile Safari */ 138 | -webkit-appearance: none; 139 | appearance: none; 140 | } 141 | 142 | .todo-list li .toggle:after { 143 | content: url('data:image/svg+xml;utf8,'); 144 | } 145 | 146 | .todo-list li .toggle:checked:after { 147 | content: url('data:image/svg+xml;utf8,'); 148 | } 149 | 150 | .todo-list li label { 151 | white-space: pre-line; 152 | word-break: break-all; 153 | padding: 15px 60px 15px 15px; 154 | margin-left: 45px; 155 | display: block; 156 | line-height: 1.2; 157 | transition: color 0.4s; 158 | } 159 | 160 | .todo-list li.completed label { 161 | color: #d9d9d9; 162 | text-decoration: line-through; 163 | } 164 | 165 | .todo-list li .destroy { 166 | display: none; 167 | position: absolute; 168 | top: 0; 169 | right: 10px; 170 | bottom: 0; 171 | width: 40px; 172 | height: 40px; 173 | margin: auto 0; 174 | font-size: 30px; 175 | color: #cc9a9a; 176 | margin-bottom: 11px; 177 | transition: color 0.2s ease-out; 178 | } 179 | 180 | .todo-list li .destroy:hover { 181 | color: #af5b5e; 182 | } 183 | 184 | .todo-list li .destroy:after { 185 | content: '×'; 186 | } 187 | 188 | .todo-list li:hover .destroy { 189 | display: block; 190 | } 191 | 192 | .todo-list li .edit { 193 | display: none; 194 | } 195 | 196 | .todo-list li.editing:last-child { 197 | margin-bottom: -1px; 198 | } 199 | 200 | .footer { 201 | color: #777; 202 | padding: 10px 15px; 203 | height: 20px; 204 | text-align: center; 205 | border-top: 1px solid #e6e6e6; 206 | } 207 | 208 | .footer:before { 209 | content: ''; 210 | position: absolute; 211 | right: 0; 212 | bottom: 0; 213 | left: 0; 214 | height: 50px; 215 | overflow: hidden; 216 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 217 | 0 8px 0 -3px #f6f6f6, 218 | 0 9px 1px -3px rgba(0, 0, 0, 0.2), 219 | 0 16px 0 -6px #f6f6f6, 220 | 0 17px 2px -6px rgba(0, 0, 0, 0.2); 221 | } 222 | 223 | .todo-count { 224 | float: left; 225 | text-align: left; 226 | } 227 | 228 | .todo-count strong { 229 | font-weight: 300; 230 | } 231 | 232 | .filters { 233 | margin: 0; 234 | padding: 0; 235 | list-style: none; 236 | position: absolute; 237 | right: 0; 238 | left: 0; 239 | } 240 | 241 | .filters li { 242 | display: inline; 243 | } 244 | 245 | .filters li a { 246 | color: inherit; 247 | margin: 3px; 248 | padding: 3px 7px; 249 | text-decoration: none; 250 | border: 1px solid transparent; 251 | border-radius: 3px; 252 | } 253 | 254 | .filters li a.selected, 255 | .filters li a:hover { 256 | border-color: rgba(175, 47, 47, 0.1); 257 | } 258 | 259 | .filters li a.selected { 260 | border-color: rgba(175, 47, 47, 0.2); 261 | } 262 | 263 | .clear-completed, 264 | html .clear-completed:active { 265 | float: right; 266 | position: relative; 267 | line-height: 20px; 268 | text-decoration: none; 269 | cursor: pointer; 270 | position: relative; 271 | } 272 | 273 | .clear-completed:hover { 274 | text-decoration: underline; 275 | } 276 | 277 | .info { 278 | margin: 65px auto 0; 279 | color: #bfbfbf; 280 | font-size: 10px; 281 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); 282 | text-align: center; 283 | } 284 | 285 | .info p { 286 | line-height: 1; 287 | } 288 | 289 | .info a { 290 | color: inherit; 291 | text-decoration: none; 292 | font-weight: 400; 293 | } 294 | 295 | .info a:hover { 296 | text-decoration: underline; 297 | } 298 | 299 | /* 300 | Hack to remove background from Mobile Safari. 301 | Can't use it globally since it destroys checkboxes in Firefox 302 | */ 303 | @media screen and (-webkit-min-device-pixel-ratio:0) { 304 | .toggle-all, 305 | .todo-list li .toggle { 306 | background: none; 307 | } 308 | 309 | .todo-list li .toggle { 310 | height: 40px; 311 | } 312 | 313 | .toggle-all { 314 | -webkit-transform: rotate(90deg); 315 | transform: rotate(90deg); 316 | -webkit-appearance: none; 317 | appearance: none; 318 | } 319 | } 320 | 321 | @media (max-width: 430px) { 322 | .footer { 323 | height: 50px; 324 | } 325 | 326 | .filters { 327 | bottom: 10px; 328 | } 329 | } 330 | 331 | 332 | button#more-frank-todos { 333 | display: block; 334 | margin: 0 auto; 335 | padding: 6px 10px 6px 10px; 336 | border-radius: 5px; 337 | cursor: pointer; 338 | background-color: #8d8; 339 | color: white; 340 | font-size: 30px; 341 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 342 | 0 12px 25px 0 rgba(0, 0, 0, 0.1); 343 | } 344 | 345 | body { 346 | margin-bottom: 50px; 347 | } 348 | -------------------------------------------------------------------------------- /examples/todomvc/resources/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Reframe Todomvc 6 | 7 | 8 | 9 | 10 |
11 |
12 | 13 | 14 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /examples/todomvc/src/todomvc/core.cljs: -------------------------------------------------------------------------------- 1 | (ns todomvc.core 2 | (:require-macros [secretary.core :refer [defroute]]) 3 | (:require [goog.events :as events] 4 | [reagent.core :as reagent] 5 | [re-frame.core :refer [dispatch dispatch-sync]] 6 | [secretary.core :as secretary] 7 | [todomvc.events] 8 | [todomvc.subs] 9 | [todomvc.views] 10 | [devtools.core :as devtools] 11 | 12 | [todomvc.frank-subs] 13 | [todomvc.frank-views] 14 | [rum.core :as rum] 15 | [re-frame.frank :as frank] 16 | [re-frame.rum :as re-rum] 17 | [org.martinklepsch.derivatives :as derivatives] 18 | ) 19 | (:import [goog History] 20 | [goog.history EventType])) 21 | 22 | 23 | ;; -- Debugging aids ---------------------------------------------------------- 24 | (devtools/install!) ;; we love https://github.com/binaryage/cljs-devtools 25 | (enable-console-print!) ;; so that println writes to `console.log` 26 | 27 | 28 | 29 | ;; Root component for our rum views 30 | ;; Inject all we need into the context 31 | 32 | (rum/defc rum-root 33 | < (re-rum/inject-frank-into-context first) 34 | (derivatives/rum-derivatives* second) 35 | re-rum/drv+frank-child-ctx 36 | [frank derivative-specs] 37 | (todomvc.frank-views/todo-app)) 38 | 39 | (defonce current-frank-todos (atom [])) 40 | 41 | (defn re-mount-all [] 42 | ;; Clean up previous mounted instances (when hot loading code) 43 | (doseq [node (-> (js/document.getElementsByClassName "frank-todos") 44 | array-seq)] 45 | (.remove node)) 46 | 47 | (doseq [frank @current-frank-todos] 48 | (let [add-frank-button (js/document.getElementById "more-frank-todos") 49 | container (doto (js/document.createElement "div") 50 | (.. -classList (add "frank-todos"))) 51 | 52 | derivative-specs (todomvc.frank-subs/make-derivative-specs frank)] 53 | 54 | (.insertBefore document.body container add-frank-button) 55 | 56 | (rum/mount (rum-root frank 57 | derivative-specs) 58 | container)))) 59 | 60 | (defn add-frank-todos [] 61 | (let [frank (frank/create)] 62 | (frank/dispatch-sync! frank [:initialise-db]) 63 | ;; add dispatcher to the routing 64 | (swap! current-frank-todos conj frank)) 65 | 66 | (re-mount-all)) 67 | 68 | (defonce only-on-initial-load 69 | (do (add-frank-todos) 70 | (doto (js/document.getElementById "more-frank-todos") 71 | (.addEventListener "click" add-frank-todos)))) 72 | 73 | 74 | 75 | ;; -- Entry Point ------------------------------------------------------------- 76 | ;; Within ../../resources/public/index.html you'll see this code 77 | ;; window.onload = function () { 78 | ;; todomvc.core.main(); 79 | ;; } 80 | ;; So this is the entry function that kicks off the app once the HTML is loaded. 81 | ;; 82 | (defn ^:export main 83 | [] 84 | ;; Put an initial value into app-db. 85 | ;; The event handler for `:initialise-db` can be found in `events.cljs` 86 | ;; Using the sync version of dispatch means that value is in 87 | ;; place before we go onto the next step. 88 | (dispatch-sync [:initialise-db]) 89 | 90 | ;; Render the UI into the HTML's
element 91 | ;; The view function `todomvc.views/todo-app` is the 92 | ;; root view for the entire UI. 93 | (reagent/render [todomvc.views/todo-app] ;; 94 | (.getElementById js/document "app")) 95 | 96 | 97 | ;; force a re-mounting of every frank todos 98 | (re-mount-all)) 99 | 100 | ;; -- Routes and History ------------------------------------------------------ 101 | ;; Although we use the secretary library below, that's mostly a historical 102 | ;; accident. You might also consider using: 103 | ;; - https://github.com/DomKM/silk 104 | ;; - https://github.com/juxt/bidi 105 | ;; We don't have a strong opinion. 106 | 107 | 108 | (defn dispatch-all [event-v] 109 | ;; Traditional re-frame dispatch for the traditional re-frame todos 110 | (dispatch event-v) 111 | ;; Dispatch to every frank we currently have 112 | (doseq [frank @current-frank-todos] 113 | (frank/dispatch! frank event-v))) 114 | 115 | (defroute "/" [] (dispatch-all [:set-showing :all])) 116 | (defroute "/:filter" [filter] (dispatch-all [:set-showing (keyword filter)])) 117 | 118 | (def history 119 | (doto (History.) 120 | (events/listen EventType.NAVIGATE 121 | (fn [event] (secretary/dispatch! (.-token event)))) 122 | (.setEnabled true))) 123 | 124 | -------------------------------------------------------------------------------- /examples/todomvc/src/todomvc/db.cljs: -------------------------------------------------------------------------------- 1 | (ns todomvc.db 2 | (:require [cljs.reader] 3 | [cljs.spec :as s] 4 | [re-frame.core :as re-frame])) 5 | 6 | 7 | ;; -- Spec -------------------------------------------------------------------- 8 | ;; 9 | ;; This is a clojure.spec specification for the value in app-db. It is like a 10 | ;; Schema. See: http://clojure.org/guides/spec 11 | ;; 12 | ;; The value in app-db should always match this spec. Only event handlers 13 | ;; can change the value in app-db so, after each event handler 14 | ;; has run, we re-check app-db for correctness (compliance with the Schema). 15 | ;; 16 | ;; How is this done? Look in events.cljs and you'll notice that all handlers 17 | ;; have an "after" interceptor which does the spec re-check. 18 | ;; 19 | ;; None of this is strictly necessary. It could be omitted. But we find it 20 | ;; good practice. 21 | 22 | (s/def ::id int?) 23 | (s/def ::title string?) 24 | (s/def ::done boolean?) 25 | (s/def ::todo (s/keys :req-un [::id ::title ::done])) 26 | (s/def ::todos (s/and ;; should use the :kind kw to s/map-of (not supported yet) 27 | (s/map-of ::id ::todo) ;; in this map, each todo is keyed by its :id 28 | #(instance? PersistentTreeMap %) ;; is a sorted-map (not just a map) 29 | )) 30 | (s/def ::showing ;; what todos are shown to the user? 31 | #{:all ;; all todos are shown 32 | :active ;; only todos whose :done is false 33 | :done ;; only todos whose :done is true 34 | }) 35 | (s/def ::db (s/keys :req-un [::todos ::showing])) 36 | 37 | ;; -- Default app-db Value --------------------------------------------------- 38 | ;; 39 | ;; When the application first starts, this will be the value put in app-db 40 | ;; Unless, of course, there are todos in the LocalStore (see further below) 41 | ;; Look in `core.cljs` for "(dispatch-sync [:initialise-db])" 42 | ;; 43 | 44 | (def default-value ;; what gets put into app-db by default. 45 | {:todos (sorted-map) ;; an empty list of todos. Use the (int) :id as the key 46 | :showing :all}) ;; show all todos 47 | 48 | 49 | ;; -- Local Storage ---------------------------------------------------------- 50 | ;; 51 | ;; Part of the todomvc challenge is to store todos in LocalStorage, and 52 | ;; on app startup, reload the todos from when the program was last run. 53 | ;; But the challenge stipulates to NOT load the setting for the "showing" 54 | ;; filter. Just the todos. 55 | ;; 56 | 57 | (def ls-key "todos-reframe") ;; localstore key 58 | (defn todos->local-store 59 | "Puts todos into localStorage" 60 | [todos] 61 | (.setItem js/localStorage ls-key (str todos))) ;; sorted-map writen as an EDN map 62 | 63 | 64 | ;; register a coeffect handler which will load a value from localstore 65 | ;; To see it used look in events.clj at the event handler for `:initialise-db` 66 | (re-frame/reg-cofx 67 | :local-store-todos 68 | (fn [cofx _] 69 | "Read in todos from localstore, and process into a map we can merge into app-db." 70 | (assoc cofx :local-store-todos 71 | (into (sorted-map) 72 | (some->> (.getItem js/localStorage ls-key) 73 | (cljs.reader/read-string) ;; stored as an EDN map. 74 | ))))) 75 | -------------------------------------------------------------------------------- /examples/todomvc/src/todomvc/events.cljs: -------------------------------------------------------------------------------- 1 | (ns todomvc.events 2 | (:require 3 | [todomvc.db :refer [default-value todos->local-store]] 4 | [re-frame.core :refer [reg-event-db reg-event-fx inject-cofx path trim-v 5 | after debug]] 6 | [cljs.spec :as s])) 7 | 8 | 9 | ;; -- Interceptors -------------------------------------------------------------- 10 | ;; 11 | 12 | (defn check-and-throw 13 | "throw an exception if db doesn't match the spec" 14 | [a-spec db] 15 | (when-not (s/valid? a-spec db) 16 | (throw (ex-info (str "spec check failed: " (s/explain-str a-spec db)) {})))) 17 | 18 | ;; Event handlers change state, that's their job. But what happens if there's 19 | ;; a bug which corrupts app state in some subtle way? This interceptor is run after 20 | ;; each event handler has finished, and it checks app-db against a spec. This 21 | ;; helps us detect event handler bugs early. 22 | (def check-spec-interceptor (after (partial check-and-throw :todomvc.db/db))) 23 | 24 | ;; this interceptor stores todos into local storage 25 | ;; we attach it to each event handler which could update todos 26 | (def ->local-store (after todos->local-store)) 27 | 28 | ;; Each event handler can have its own set of interceptors (middleware) 29 | ;; But we use the same set of interceptors for all event habdlers related 30 | ;; to manipulating todos. 31 | ;; A chain of interceptors is a vector. 32 | (def todo-interceptors [check-spec-interceptor ;; ensure the spec is still valid 33 | (path :todos) ;; 1st param to handler will be the value from this path 34 | ->local-store ;; write todos to localstore 35 | (when ^boolean js/goog.DEBUG debug) ;; look in your browser console for debug logs 36 | trim-v]) ;; removes first (event id) element from the event vec 37 | 38 | 39 | ;; -- Helpers ----------------------------------------------------------------- 40 | 41 | (defn allocate-next-id 42 | "Returns the next todo id. 43 | Assumes todos are sorted. 44 | Returns one more than the current largest id." 45 | [todos] 46 | ((fnil inc 0) (last (keys todos)))) 47 | 48 | 49 | ;; -- Event Handlers ---------------------------------------------------------- 50 | 51 | ;; usage: (dispatch [:initialise-db]) 52 | (reg-event-fx ;; on app startup, create initial state 53 | :initialise-db ;; event id being handled 54 | [(inject-cofx :local-store-todos) ;; obtain todos from localstore 55 | check-spec-interceptor] ;; after the event handler runs, check that app-db matches the spec 56 | (fn [{:keys [db local-store-todos]} _] ;; the handler being registered 57 | {:db (assoc default-value :todos local-store-todos)})) ;; all hail the new state 58 | 59 | 60 | ;; usage: (dispatch [:set-showing :active]) 61 | (reg-event-db ;; this handler changes the todo filter 62 | :set-showing ;; event-id 63 | 64 | ;; this chain of two interceptors wrap the handler 65 | [check-spec-interceptor (path :showing) trim-v] 66 | 67 | ;; The event handler 68 | ;; Because of the path interceptor above, the 1st parameter to 69 | ;; the handler below won't be the entire 'db', and instead will 70 | ;; be the value at a certain path within db, namely :showing. 71 | ;; Also, the use of the 'trim-v' interceptor means we can omit 72 | ;; the leading underscore from the 2nd parameter (event vector). 73 | (fn [old-keyword [new-filter-kw]] ;; handler 74 | new-filter-kw)) ;; return new state for the path 75 | 76 | 77 | ;; usage: (dispatch [:add-todo "Finish comments"]) 78 | (reg-event-db ;; given the text, create a new todo 79 | :add-todo 80 | 81 | ;; The standard set of interceptors, defined above, which we 82 | ;; apply to all todos-modifiing event handlers. Looks after 83 | ;; writing todos to local store, etc. 84 | todo-interceptors 85 | 86 | ;; The event handler function. 87 | ;; The "path" interceptor in `todo-interceptors` means 1st parameter is :todos 88 | (fn [todos [text]] 89 | (let [id (allocate-next-id todos)] 90 | (assoc todos id {:id id :title text :done false})))) 91 | 92 | 93 | (reg-event-db 94 | :toggle-done 95 | todo-interceptors 96 | (fn [todos [id]] 97 | (update-in todos [id :done] not))) 98 | 99 | 100 | (reg-event-db 101 | :save 102 | todo-interceptors 103 | (fn [todos [id title]] 104 | (assoc-in todos [id :title] title))) 105 | 106 | 107 | (reg-event-db 108 | :delete-todo 109 | todo-interceptors 110 | (fn [todos [id]] 111 | (dissoc todos id))) 112 | 113 | 114 | (reg-event-db 115 | :clear-completed 116 | todo-interceptors 117 | (fn [todos _] 118 | (->> (vals todos) ;; find the ids of all todos where :done is true 119 | (filter :done) 120 | (map :id) 121 | (reduce dissoc todos)))) ;; now delete these ids 122 | 123 | 124 | (reg-event-db 125 | :complete-all-toggle 126 | todo-interceptors 127 | (fn [todos _] 128 | (let [new-done (not-every? :done (vals todos))] ;; work out: toggle true or false? 129 | (reduce #(assoc-in %1 [%2 :done] new-done) 130 | todos 131 | (keys todos))))) 132 | -------------------------------------------------------------------------------- /examples/todomvc/src/todomvc/frank_subs.cljs: -------------------------------------------------------------------------------- 1 | (ns todomvc.frank-subs) 2 | 3 | (defn sorted-todos [base] 4 | (:todos base)) 5 | 6 | (defn make-derivative-specs [base] 7 | {:base [[] base] 8 | :showing [[:base] :showing] 9 | :sorted-todos [[:base] sorted-todos] 10 | :todos [[:sorted-todos] vals] 11 | 12 | :visible-todos [[:todos :showing] 13 | (fn [todos showing] 14 | (let [filter-fn (case showing 15 | :active (complement :done) 16 | :done :done 17 | :all identity)] 18 | (filter filter-fn todos)))] 19 | 20 | ;; The name of this derivation (and the original subscription which inspired 21 | ;; it) is misleading. 22 | :all-complete? [[:todos] #(seq %)] 23 | :completed-count [[:todos] #(count (filter :done %))] 24 | 25 | :footer-counts [[:todos :completed-count] 26 | (fn [todos completed] 27 | [(- (count todos) completed) completed])]}) 28 | -------------------------------------------------------------------------------- /examples/todomvc/src/todomvc/frank_views.cljs: -------------------------------------------------------------------------------- 1 | (ns todomvc.frank-views 2 | (:require [rum.core :as rum] 3 | [re-frame.rum :as re-rum] 4 | [org.martinklepsch.derivatives :as d])) 5 | 6 | 7 | (rum/defcs todo-input 8 | < (rum/local "TODO this should come from the props" ::title) 9 | {:will-mount (fn [{title ::title :as rum-state}] 10 | (reset! title 11 | (-> rum-state :rum/args first :title)) 12 | rum-state)} 13 | [{val ::title :as rum-state} 14 | {:keys [title on-save on-stop] :as props}] 15 | (let [stop #(do (reset! val "") 16 | (when on-stop (on-stop))) 17 | save #(let [v (-> @val str clojure.string/trim)] 18 | (when (seq v) (on-save v)) 19 | (stop))] 20 | [:input (merge (dissoc props :on-save :on-stop) 21 | {:type "text" 22 | :value @val 23 | :auto-focus true 24 | :on-blur save 25 | :on-change #(reset! val (-> % .-target .-value)) 26 | :on-key-down #(case (.-which %) 27 | 13 (save) 28 | 27 (stop) 29 | nil)})])) 30 | 31 | (rum/defcs todo-item 32 | < re-rum/with-frank 33 | (rum/local false ::editing) 34 | {:key-fn (fn [{:keys [id]}] id)} 35 | [{dispatch! :frank/dispatch! 36 | editing ::editing} 37 | {:keys [id done title]}] 38 | [:li {:class (str (when done "completed ") 39 | (when @editing "editing"))} 40 | [:div.view 41 | [:input.toggle 42 | {:type "checkbox" 43 | :checked done 44 | :on-change #(dispatch! [:toggle-done id])}] 45 | [:label 46 | {:on-double-click #(reset! editing true)} 47 | title] 48 | [:button.destroy 49 | {:on-click #(dispatch! [:delete-todo id])}]] 50 | (when @editing 51 | (todo-input {:class "edit" 52 | :title title 53 | :on-save #(dispatch! [:save id %]) 54 | :on-stop #(reset! editing false)}))]) 55 | 56 | 57 | 58 | (rum/defcs task-list 59 | < re-rum/with-frank 60 | rum/reactive 61 | (d/drv :visible-todos :all-complete?) 62 | re-rum/drv+frank-ctx 63 | [{dispatch! :frank/dispatch! :as rum-state}] 64 | (let [visible-todos (d/react rum-state :visible-todos) 65 | all-complete? (d/react rum-state :all-complete?)] 66 | [:section.main 67 | [:input.toggle-all 68 | {:type "checkbox" 69 | :checked all-complete? 70 | :on-change #(dispatch! [:complete-all-toggle (not all-complete?)])}] 71 | [:label 72 | {:for "toggle-all"} 73 | "Mark all as complete"] 74 | [:ul.todo-list 75 | (for [todo visible-todos] 76 | (todo-item todo))]])) 77 | 78 | 79 | (rum/defcs footer-controls 80 | < re-rum/with-frank 81 | rum/reactive 82 | (d/drv :footer-counts :showing) 83 | re-rum/drv+frank-ctx 84 | [{dispatch! :frank/dispatch! :as rum-state}] 85 | (let [[active done] (d/react rum-state :footer-counts) 86 | showing (d/react rum-state :showing) 87 | a-fn (fn [filter-kw txt] 88 | [:a {:class (when (= filter-kw showing) "selected") 89 | :href (str "#/" (name filter-kw))} txt])] 90 | [:footer.footer 91 | [:span.todo-count 92 | [:strong active] " " (case active 1 "item" "items") " left"] 93 | [:ul.filters 94 | [:li (a-fn :all "All")] 95 | [:li (a-fn :active "Active")] 96 | [:li (a-fn :done "Completed")]] 97 | (when (pos? done) 98 | [:button.clear-completed {:on-click #(dispatch! [:clear-completed])} 99 | "Clear completed"])])) 100 | 101 | (rum/defcs task-entry 102 | < re-rum/with-frank 103 | [{dispatch! :frank/dispatch!}] 104 | [:header.header 105 | [:h1 {:style {:color "#8d8"}} "todos"] 106 | (todo-input {:class "new-todo" 107 | :placeholder "What needs to be done?" 108 | :on-save #(dispatch! [:add-todo %])})]) 109 | 110 | (rum/defcs todo-app 111 | < rum/reactive 112 | (d/drv :todos) 113 | [rum-state] 114 | [:div 115 | [:section.todoapp 116 | (task-entry) 117 | (when (seq (d/react rum-state :todos)) 118 | (task-list)) 119 | (footer-controls)] 120 | [:footer.info 121 | [:p "Double-click to edit a todo"]]]) 122 | -------------------------------------------------------------------------------- /examples/todomvc/src/todomvc/subs.cljs: -------------------------------------------------------------------------------- 1 | (ns todomvc.subs 2 | (:require [re-frame.core :refer [reg-sub subscribe]])) 3 | 4 | ;; ------------------------------------------------------------------------------------- 5 | ;; Layer 2 (see the Subscriptions Infographic for meaning) 6 | ;; 7 | (reg-sub 8 | :showing 9 | (fn [db _] ;; db is the (map) value in app-db 10 | (:showing db))) ;; I repeat: db is a value. Not a ratom. And this fn does not return a reaction, just a value. 11 | 12 | ;; that `fn` is a pure function 13 | 14 | ;; Next, the registration of a similar handler is done in two steps. 15 | ;; First, we `defn` a pure handler function. Then, we use `reg-sub` to register it. 16 | ;; Two steps. This is different to that first registration, above, which was done in one step. 17 | (defn sorted-todos 18 | [db _] 19 | (:todos db)) 20 | (reg-sub :sorted-todos sorted-todos) 21 | 22 | ;; ------------------------------------------------------------------------------------- 23 | ;; Layer 3 (see the infographic for meaning) 24 | ;; 25 | ;; A subscription handler is a function which is re-run when its input signals 26 | ;; change. Each time it is rerun, it produces a new output (return value). 27 | ;; 28 | ;; In the simple case, app-db is the only input signal, as was the case in the two 29 | ;; simple subscriptions above. But many subscriptions are not directly dependent on 30 | ;; app-db, and instead, depend on a value derived from app-db. 31 | ;; 32 | ;; Such handlers represent "intermediate nodes" in a signal graph. New values emanate 33 | ;; from app-db, and flow out through a signal graph, into and out of these intermediate 34 | ;; nodes, before a leaf subscription delivers data into views which render data as hiccup. 35 | ;; 36 | ;; When writing and registering the handler for an intermediate node, you must nominate 37 | ;; one or more input signals (typically one or two). 38 | ;; 39 | ;; reg-sub allows you to supply: 40 | ;; 41 | ;; 1. a function which returns the input signals. It can return either a single signal or 42 | ;; a vector of signals, or a map of where the values are the signals. 43 | ;; 44 | ;; 2. a function which does the computation. It takes input values and produces a new 45 | ;; derived value. 46 | ;; 47 | ;; In the two simple examples at the top, we only supplied the 2nd of these functions. 48 | ;; But now we are dealing with intermediate nodes, we'll need to provide both fns. 49 | ;; 50 | (reg-sub 51 | :todos 52 | 53 | ;; This function returns the input signals. 54 | ;; In this case, it returns a single signal. 55 | ;; Although not required in this example, it is called with two paramters 56 | ;; being the two values supplied in the originating `(subscribe X Y)`. 57 | ;; X will be the query vector and Y is an advanced feature and out of scope 58 | ;; for this explanation. 59 | (fn [query-v _] 60 | (subscribe [:sorted-todos])) ;; returns a single input signal 61 | 62 | ;; This 2nd fn does the computation. Data values in, derived data out. 63 | ;; It is the same as the two simple subscription handlers up at the top. 64 | ;; Except they took the value in app-db as their first argument and, instead, 65 | ;; this function takes the value delivered by another input signal, supplied by the 66 | ;; function above: (subscribe [:sorted-todos]) 67 | ;; 68 | ;; Subscription handlers can take 3 parameters: 69 | ;; - the input signals (a single item, a vector or a map) 70 | ;; - the query vector supplied to query-v (the query vector argument 71 | ;; to the "subscribe") and the 3rd one is for advanced cases, out of scope for this discussion. 72 | (fn [sorted-todos query-v _] 73 | (vals sorted-todos))) 74 | 75 | ;; So here we define the handler for another intermediate node. 76 | ;; This time the computation involves two input signals. 77 | ;; As a result note: 78 | ;; - the first function (which returns the signals, returns a 2-vector) 79 | ;; - the second function (which is the computation, destructures this 2-vector as its first parameter) 80 | (reg-sub 81 | :visible-todos 82 | 83 | ;; signal function 84 | ;; returns a vector of two input signals 85 | (fn [query-v _] 86 | [(subscribe [:todos]) 87 | (subscribe [:showing])]) 88 | 89 | ;; computation function 90 | (fn [[todos showing] _] ;; that 1st parameter is a 2-vector of values 91 | (let [filter-fn (case showing 92 | :active (complement :done) 93 | :done :done 94 | :all identity)] 95 | (filter filter-fn todos)))) 96 | 97 | ;; ------------------------------------------------------------------------------------- 98 | ;; Hey, wait on!! 99 | ;; 100 | ;; How did those two simple registrations at the top work? 101 | ;; We only supplied one function in those registrations, not two? 102 | ;; Very observant of you, I'm glad you asked. 103 | ;; When the signal-returning-fn is omitted, re-sub provides a default, 104 | ;; and it looks like this: 105 | ;; (fn [_ _] re-frame.db/app-db) 106 | ;; It returns one signal, and that signal is app-db itself. 107 | ;; 108 | ;; So the two simple registrations at the top didn't need to provide a signal-fn, 109 | ;; because they operated only on the value in app-db, supplied as 'db' in the 1st arguement. 110 | 111 | ;; ------------------------------------------------------------------------------------- 112 | ;; SUGAR ? 113 | ;; Now for some syntactic sugar... 114 | ;; The purpose of the sugar is to remove boilerplate noise. To distill to the essential 115 | ;; in 90% of cases. 116 | ;; Because it is so common to nominate 1 or more input signals, 117 | ;; reg-sub provides some macro sugar so you can nominate a very minimal 118 | ;; vector of input signals. The 1st function is not needed. 119 | ;; Here is the example above rewritten using the sugar. 120 | #_(reg-sub 121 | :visible-todos 122 | :<- [:todos] 123 | :<- [:showing] 124 | (fn [[todos showing] _] 125 | (let [filter-fn (case showing 126 | :active (complement :done) 127 | :done :done 128 | :all identity)] 129 | (filter filter-fn todos)))) 130 | 131 | 132 | (reg-sub 133 | :all-complete? 134 | :<- [:todos] 135 | (fn [todos _] 136 | (seq todos))) 137 | 138 | (reg-sub 139 | :completed-count 140 | :<- [:todos] 141 | (fn [todos _] 142 | (count (filter :done todos)))) 143 | 144 | (reg-sub 145 | :footer-counts 146 | :<- [:todos] 147 | :<- [:completed-count] 148 | (fn [[todos completed] _] 149 | [(- (count todos) completed) completed])) 150 | -------------------------------------------------------------------------------- /examples/todomvc/src/todomvc/views.cljs: -------------------------------------------------------------------------------- 1 | (ns todomvc.views 2 | (:require [reagent.core :as reagent] 3 | [re-frame.core :refer [subscribe dispatch]])) 4 | 5 | 6 | (defn todo-input [{:keys [title on-save on-stop]}] 7 | (let [val (reagent/atom title) 8 | stop #(do (reset! val "") 9 | (when on-stop (on-stop))) 10 | save #(let [v (-> @val str clojure.string/trim)] 11 | (when (seq v) (on-save v)) 12 | (stop))] 13 | (fn [props] 14 | [:input (merge (dissoc props :on-save :on-stop) 15 | {:type "text" 16 | :value @val 17 | :auto-focus true 18 | :on-blur save 19 | :on-change #(reset! val (-> % .-target .-value)) 20 | :on-key-down #(case (.-which %) 21 | 13 (save) 22 | 27 (stop) 23 | nil)})]))) 24 | 25 | 26 | (defn todo-item 27 | [] 28 | (let [editing (reagent/atom false)] 29 | (fn [{:keys [id done title]}] 30 | [:li {:class (str (when done "completed ") 31 | (when @editing "editing"))} 32 | [:div.view 33 | [:input.toggle 34 | {:type "checkbox" 35 | :checked done 36 | :on-change #(dispatch [:toggle-done id])}] 37 | [:label 38 | {:on-double-click #(reset! editing true)} 39 | title] 40 | [:button.destroy 41 | {:on-click #(dispatch [:delete-todo id])}]] 42 | (when @editing 43 | [todo-input 44 | {:class "edit" 45 | :title title 46 | :on-save #(dispatch [:save id %]) 47 | :on-stop #(reset! editing false)}])]))) 48 | 49 | 50 | (defn task-list 51 | [] 52 | (let [visible-todos @(subscribe [:visible-todos]) 53 | all-complete? @(subscribe [:all-complete?])] 54 | [:section#main 55 | [:input#toggle-all 56 | {:type "checkbox" 57 | :checked all-complete? 58 | :on-change #(dispatch [:complete-all-toggle (not all-complete?)])}] 59 | [:label 60 | {:for "toggle-all"} 61 | "Mark all as complete"] 62 | [:ul#todo-list 63 | (for [todo visible-todos] 64 | ^{:key (:id todo)} [todo-item todo])]])) 65 | 66 | 67 | (defn footer-controls 68 | [] 69 | (let [[active done] @(subscribe [:footer-counts]) 70 | showing @(subscribe [:showing]) 71 | a-fn (fn [filter-kw txt] 72 | [:a {:class (when (= filter-kw showing) "selected") 73 | :href (str "#/" (name filter-kw))} txt])] 74 | [:footer#footer 75 | [:span#todo-count 76 | [:strong active] " " (case active 1 "item" "items") " left"] 77 | [:ul#filters 78 | [:li (a-fn :all "All")] 79 | [:li (a-fn :active "Active")] 80 | [:li (a-fn :done "Completed")]] 81 | (when (pos? done) 82 | [:button#clear-completed {:on-click #(dispatch [:clear-completed])} 83 | "Clear completed"])])) 84 | 85 | 86 | (defn task-entry 87 | [] 88 | [:header#header 89 | [:h1 "todos"] 90 | [todo-input 91 | {:id "new-todo" 92 | :placeholder "What needs to be done?" 93 | :on-save #(dispatch [:add-todo %])}]]) 94 | 95 | 96 | (defn todo-app 97 | [] 98 | [:div 99 | [:section#todoapp 100 | [task-entry] 101 | (when (seq @(subscribe [:todos])) 102 | [task-list]) 103 | [footer-controls]] 104 | [:footer#info 105 | [:p "Double-click to edit a todo"]]]) 106 | -------------------------------------------------------------------------------- /images/Readme/6dominoes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chpill/re-frankenstein/c3b84969088f78aa00f4cfcef13338d8cb9a279e/images/Readme/6dominoes.png -------------------------------------------------------------------------------- /images/Readme/Dominoes-small.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chpill/re-frankenstein/c3b84969088f78aa00f4cfcef13338d8cb9a279e/images/Readme/Dominoes-small.jpg -------------------------------------------------------------------------------- /images/Readme/Dominoes.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chpill/re-frankenstein/c3b84969088f78aa00f4cfcef13338d8cb9a279e/images/Readme/Dominoes.jpg -------------------------------------------------------------------------------- /images/Readme/todolist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chpill/re-frankenstein/c3b84969088f78aa00f4cfcef13338d8cb9a279e/images/Readme/todolist.png -------------------------------------------------------------------------------- /images/event-handlers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chpill/re-frankenstein/c3b84969088f78aa00f4cfcef13338d8cb9a279e/images/event-handlers.png -------------------------------------------------------------------------------- /images/example_app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chpill/re-frankenstein/c3b84969088f78aa00f4cfcef13338d8cb9a279e/images/example_app.png -------------------------------------------------------------------------------- /images/logo/Genesis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chpill/re-frankenstein/c3b84969088f78aa00f4cfcef13338d8cb9a279e/images/logo/Genesis.png -------------------------------------------------------------------------------- /images/logo/Guggenheim.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chpill/re-frankenstein/c3b84969088f78aa00f4cfcef13338d8cb9a279e/images/logo/Guggenheim.jpg -------------------------------------------------------------------------------- /images/logo/README.md: -------------------------------------------------------------------------------- 1 | 2 | ![logo](/images/logo/re-frame_256w.png?raw=true) 3 | 4 | [Read the backstory here.](/docs/The-re-frame-logo.md) 5 | 6 | Created via [Sketch.app](https://www.sketchapp.com/). See the file `re-frame-logo.sketch` 7 | 8 | Unfortunately the gradients are not exported properly so we can't provide an SVG here for now. 9 | -------------------------------------------------------------------------------- /images/logo/frame_1024w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chpill/re-frankenstein/c3b84969088f78aa00f4cfcef13338d8cb9a279e/images/logo/frame_1024w.png -------------------------------------------------------------------------------- /images/logo/re-frame-logo.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chpill/re-frankenstein/c3b84969088f78aa00f4cfcef13338d8cb9a279e/images/logo/re-frame-logo.sketch -------------------------------------------------------------------------------- /images/logo/re-frame_128w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chpill/re-frankenstein/c3b84969088f78aa00f4cfcef13338d8cb9a279e/images/logo/re-frame_128w.png -------------------------------------------------------------------------------- /images/logo/re-frame_256w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chpill/re-frankenstein/c3b84969088f78aa00f4cfcef13338d8cb9a279e/images/logo/re-frame_256w.png -------------------------------------------------------------------------------- /images/logo/re-frame_512w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chpill/re-frankenstein/c3b84969088f78aa00f4cfcef13338d8cb9a279e/images/logo/re-frame_512w.png -------------------------------------------------------------------------------- /images/logo/re-frankenstein-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chpill/re-frankenstein/c3b84969088f78aa00f4cfcef13338d8cb9a279e/images/logo/re-frankenstein-logo.png -------------------------------------------------------------------------------- /images/mental-model-omnibus.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chpill/re-frankenstein/c3b84969088f78aa00f4cfcef13338d8cb9a279e/images/mental-model-omnibus.jpg -------------------------------------------------------------------------------- /images/scale-changes-everything.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chpill/re-frankenstein/c3b84969088f78aa00f4cfcef13338d8cb9a279e/images/scale-changes-everything.jpg -------------------------------------------------------------------------------- /images/subscriptions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chpill/re-frankenstein/c3b84969088f78aa00f4cfcef13338d8cb9a279e/images/subscriptions.png -------------------------------------------------------------------------------- /images/the-water-cycle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chpill/re-frankenstein/c3b84969088f78aa00f4cfcef13338d8cb9a279e/images/the-water-cycle.png -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | var root = 'run/compiled/karma/test'; // same as :output-dir 3 | var junitOutputDir = process.env.CIRCLE_TEST_REPORTS || "run/compiled/karma/test/junit"; 4 | 5 | config.set({ 6 | frameworks: ['cljs-test'], 7 | browsers: ['Chrome'], 8 | files: [ 9 | root + '/../test.js', // same as :output-to 10 | {pattern: root + '/../test.js.map', included: false, watched: false}, 11 | {pattern: root + '/**/*.+(cljs|cljc|clj|js|js.map)', included: false, watched: false} 12 | ], 13 | 14 | client: { 15 | args: ['re_frame.test_runner.run_karma'] 16 | }, 17 | 18 | autoWatchBatchDelay: 500, 19 | 20 | // the default configuration 21 | junitReporter: { 22 | outputDir: junitOutputDir + '/karma', // results will be saved as $outputDir/$browserName.xml 23 | outputFile: undefined, // if included, results will be saved as $outputDir/$browserName/$outputFile 24 | suite: '' // suite will become the package name attribute in xml testsuite element 25 | } 26 | }) 27 | }; 28 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2017 Michael Thompson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject com.chpill.re-frankenstein "0.0.3-SNAPSHOT" 2 | :description "The deviant fork of re-frame" 3 | :url "https://github.com/chpill/re-frankenstein.git" 4 | :license {:name "MIT"} 5 | :dependencies [[org.clojure/clojure "1.8.0"] 6 | [org.clojure/clojurescript "1.9.227"] 7 | [reagent "0.6.0"] 8 | [net.cgrand/macrovich "0.2.0"] 9 | [org.clojure/tools.logging "0.3.1"] 10 | [rum "0.10.8"]] 11 | 12 | :profiles {:debug {:debug true} 13 | :dev {:dependencies [[karma-reporter "1.0.1"] 14 | [binaryage/devtools "0.8.1"] 15 | [cljsjs/react-dom-server "15.4.2-2"] 16 | [rum "0.10.8"]] 17 | :plugins [[lein-ancient "0.6.10"] 18 | [lein-cljsbuild "1.1.4"] 19 | [lein-npm "0.6.2"] 20 | [lein-figwheel "0.5.6"] 21 | [lein-shell "0.5.0"]]}} 22 | 23 | :clean-targets [:target-path "run/compiled"] 24 | 25 | :resource-paths ["run/resources"] 26 | :jvm-opts ["-Xmx1g" "-XX:+UseConcMarkSweepGC"] 27 | :source-paths ["src"] 28 | :test-paths ["test"] 29 | 30 | :shell {:commands {"open" {:windows ["cmd" "/c" "start"] 31 | :macosx "open" 32 | :linux "xdg-open"}}} 33 | 34 | :deploy-repositories [["releases" {:sign-releases false :url "https://clojars.org/repo"}] 35 | ["snapshots" {:sign-releases false :url "https://clojars.org/repo"}]] 36 | 37 | :release-tasks [["vcs" "assert-committed"] 38 | ["change" "version" "leiningen.release/bump-version" "release"] 39 | ["vcs" "commit"] 40 | ["vcs" "tag" "v" "--no-sign"] 41 | ["deploy"] 42 | ["change" "version" "leiningen.release/bump-version"] 43 | ["vcs" "commit"] 44 | ["vcs" "push"]] 45 | 46 | :npm {:devDependencies [[karma "1.0.0"] 47 | [karma-cljs-test "0.1.0"] 48 | [karma-chrome-launcher "0.2.0"] 49 | [karma-junit-reporter "0.3.8"]]} 50 | 51 | :cljsbuild {:builds [{:id "test" 52 | :source-paths ["test" "src"] 53 | :compiler {:preloads [devtools.preload] 54 | :external-config {:devtools/config {:features-to-install [:formatters :hints]}} 55 | :output-to "run/compiled/browser/test.js" 56 | :source-map true 57 | :output-dir "run/compiled/browser/test" 58 | :optimizations :none 59 | :source-map-timestamp true 60 | :pretty-print true}} 61 | {:id "karma" 62 | :source-paths ["test" "src"] 63 | :compiler {:output-to "run/compiled/karma/test.js" 64 | :source-map "run/compiled/karma/test.js.map" 65 | :output-dir "run/compiled/karma/test" 66 | :optimizations :whitespace 67 | :main "re_frame.test_runner" 68 | :pretty-print true 69 | :closure-defines {"re_frame.trace.trace_enabled_QMARK_" true}}}]} 70 | 71 | :aliases {"test-once" ["do" "clean," "cljsbuild" "once" "test," "shell" "open" "test/test.html"] 72 | "test-auto" ["do" "clean," "cljsbuild" "auto" "test,"] 73 | "karma-once" ["do" "clean," "cljsbuild" "once" "karma,"] 74 | "karma-auto" ["do" "clean," "cljsbuild" "auto" "karma,"]}) 75 | -------------------------------------------------------------------------------- /re-frankenstein-CHANGES.md: -------------------------------------------------------------------------------- 1 | ## 0.0.3-SNAPSHOT 2 | 3 | - Rebased on re-frame@v0.9.4 4 | - You can now supply your own state atom when creating a frank 5 | 6 | #### Breaking 7 | 8 | - Effect handlers get a map as second argument containing the dispatch and 9 | dispatch-sync functions 10 | 11 | 12 | ## 0.0.2 (19/05/2017) 13 | 14 | #### Breaking 15 | 16 | - Effects handlers to be used on a `frank` instance should now be a function of 17 | 2 arguments: 18 | * the effect value 19 | * the dispatch function to use in the handler 20 | 21 | See the [doc](Docs/re-frankenstein/about-effects) for more details. 22 | 23 | ## 0.0.1 (17/05/2017) 24 | 25 | Original release 26 | -------------------------------------------------------------------------------- /src/re_frame/cofx.cljc: -------------------------------------------------------------------------------- 1 | (ns re-frame.cofx 2 | (:require 3 | [re-frame.db :refer [app-db]] 4 | [re-frame.interceptor :refer [->interceptor]] 5 | [re-frame.registrar :refer [get-handler clear-handlers register-handler]] 6 | [re-frame.loggers :refer [console]])) 7 | 8 | 9 | ;; -- Registration ------------------------------------------------------------ 10 | 11 | (def kind :cofx) 12 | (assert (re-frame.registrar/kinds kind)) 13 | (def register (partial register-handler kind)) 14 | 15 | 16 | ;; -- Interceptor ------------------------------------------------------------- 17 | 18 | (defn inject-cofx 19 | "Returns an interceptor which adds to a `context's` `:coeffects`. 20 | 21 | `coeffects` are the input resources required by an event handler 22 | to perform its job. The two most obvious ones are `db` and `event`. 23 | But sometimes a handler might need other resources. 24 | 25 | Perhaps a handler needs a random number or a GUID or the current datetime. 26 | Perhaps it needs access to the connection to a DataScript database. 27 | 28 | If the handler directly access these resources, it stops being as 29 | pure. It immedaitely becomes harder to test, etc. 30 | 31 | So the necessary resources are \"injected\" into the `coeffect` (map) 32 | given the handler. 33 | 34 | Given an `id`, and an optional value, lookup the registered coeffect 35 | handler (previously registered via `reg-cofx`) and it with two arguments: 36 | the current value of `:coeffects` and, optionally, the value. The registered handler 37 | is expected to return a modified coeffect. 38 | " 39 | ([id] 40 | (->interceptor 41 | :id (keyword "coeffects" (name id)) 42 | :before (fn coeffects-before 43 | [context] 44 | (update context :coeffects (get-handler kind id))))) 45 | ([id value] 46 | (->interceptor 47 | :id (keyword "coeffects" (name id)) 48 | :before (fn coeffects-before 49 | [context] 50 | (update context :coeffects (get-handler kind id) value))))) 51 | 52 | 53 | ;; -- Builtin CoEffects Handlers --------------------------------------------- 54 | 55 | ;; :db 56 | ;; 57 | ;; Adds to coeffects the value in `app-db`, under the key `:db` 58 | (register 59 | :db 60 | (fn db-coeffects-handler 61 | [coeffects] 62 | (assoc coeffects :db @app-db))) 63 | 64 | 65 | ;; Because this interceptor is used so much, we reify it 66 | (def inject-db (inject-cofx :db)) 67 | 68 | 69 | -------------------------------------------------------------------------------- /src/re_frame/core.cljc: -------------------------------------------------------------------------------- 1 | (ns re-frame.core 2 | (:require 3 | [re-frame.events :as events] 4 | [re-frame.subs :as subs] 5 | [re-frame.interop :as interop] 6 | [re-frame.db :as db] 7 | [re-frame.fx :as fx] 8 | [re-frame.cofx :as cofx] 9 | [re-frame.router :as router] 10 | [re-frame.loggers :as loggers] 11 | [re-frame.registrar :as registrar] 12 | [re-frame.interceptor :as interceptor] 13 | [re-frame.std-interceptors :as std-interceptors :refer [db-handler->interceptor 14 | fx-handler->interceptor 15 | ctx-handler->interceptor]] 16 | [clojure.set :as set])) 17 | 18 | 19 | ;; -- dispatch 20 | (def dispatch router/dispatch) 21 | (def dispatch-sync router/dispatch-sync) 22 | 23 | 24 | ;; XXX move API functions up to this core level - to enable code completion and docs 25 | ;; XXX on figwheel reload, is there a way to not get the re-registration messages. 26 | 27 | 28 | ;; -- interceptor related 29 | ;; useful if you are writing your own interceptors 30 | (def ->interceptor interceptor/->interceptor) 31 | (def enqueue interceptor/enqueue) 32 | (def get-coeffect interceptor/get-coeffect) 33 | (def get-effect interceptor/get-effect) 34 | (def assoc-effect interceptor/assoc-effect) 35 | (def assoc-coeffect interceptor/assoc-coeffect) 36 | 37 | 38 | ;; -- standard interceptors 39 | (def debug std-interceptors/debug) 40 | (def path std-interceptors/path) 41 | (def enrich std-interceptors/enrich) 42 | (def trim-v std-interceptors/trim-v) 43 | (def after std-interceptors/after) 44 | (def on-changes std-interceptors/on-changes) 45 | 46 | 47 | ;; -- subscriptions 48 | (defn reg-sub-raw 49 | "Associate a given `query id` with a given subscription handler function `handler-fn` 50 | which is expected to take two arguments: app-db and query vector, and return 51 | a `reaction`. 52 | 53 | This is a low level, advanced function. You should probably be using reg-sub 54 | instead." 55 | [query-id handler-fn] 56 | (registrar/register-handler subs/kind query-id handler-fn)) 57 | 58 | (def reg-sub subs/reg-sub) 59 | (def subscribe subs/subscribe) 60 | 61 | (def clear-sub (partial registrar/clear-handlers subs/kind)) 62 | (def clear-subscription-cache! subs/clear-subscription-cache!) 63 | 64 | ;; -- effects 65 | (def reg-fx fx/register) 66 | (def clear-fx (partial registrar/clear-handlers fx/kind)) 67 | 68 | ;; -- coeffects 69 | (def reg-cofx cofx/register) 70 | (def inject-cofx cofx/inject-cofx) 71 | (def clear-cofx (partial registrar/clear-handlers cofx/kind)) 72 | 73 | 74 | ;; -- Events 75 | (def clear-event (partial registrar/clear-handlers events/kind)) 76 | 77 | (defn reg-event-db 78 | "Register the given `id`, typically a keyword, with the combination of 79 | `db-handler` and an interceptor chain. 80 | `db-handler` is a function: (db event) -> db 81 | `interceptors` is a collection of interceptors, possibly nested (needs flattening). 82 | `db-handler` is wrapped in an interceptor and added to the end of the chain, so in the end 83 | there is only a chain. 84 | The necessary effects and coeffects handler are added to the front of the 85 | interceptor chain. These interceptors ensure that app-db is available and updated." 86 | ([id db-handler] 87 | (reg-event-db id nil db-handler)) 88 | ([id interceptors db-handler] 89 | (events/register id [cofx/inject-db fx/do-fx interceptors (db-handler->interceptor db-handler)]))) 90 | 91 | 92 | (defn reg-event-fx 93 | ([id fx-handler] 94 | (reg-event-fx id nil fx-handler)) 95 | ([id interceptors fx-handler] 96 | (events/register id [cofx/inject-db fx/do-fx interceptors (fx-handler->interceptor fx-handler)]))) 97 | 98 | 99 | (defn reg-event-ctx 100 | ([id handler] 101 | (reg-event-ctx id nil handler)) 102 | ([id interceptors handler] 103 | (events/register id [cofx/inject-db fx/do-fx interceptors (ctx-handler->interceptor handler)]))) 104 | 105 | 106 | ;; -- Logging ----- 107 | ;; Internally, re-frame uses the logging functions: warn, log, error, group and groupEnd 108 | ;; By default, these functions map directly to the js/console implementations, 109 | ;; but you can override with your own fns (set or subset). 110 | ;; Example Usage: 111 | ;; (defn my-fn [& args] (post-it-somewhere (apply str args))) ;; here is my alternative 112 | ;; (re-frame.core/set-loggers! {:warn my-fn :log my-fn}) ;; override the defaults with mine 113 | (def set-loggers! loggers/set-loggers!) 114 | 115 | ;; If you are writing an extension to re-frame, like perhaps 116 | ;; an effects handler, you may want to use re-frame logging. 117 | ;; 118 | ;; usage: (console :error "this is bad: " a-variable " and " anotherv) 119 | ;; (console :warn "possible breach of containment wall at: " dt) 120 | (def console loggers/console) 121 | 122 | 123 | ;; -- State Restoration For Unit Tests 124 | 125 | (defn make-restore-fn 126 | "Checkpoints the state of re-frame and returns a function which, when 127 | later called, will restore re-frame to that checkpointed state. 128 | 129 | Checkpoint includes app-db, all registered handlers and all subscriptions. 130 | " 131 | [] 132 | (let [handlers @registrar/kind->id->handler 133 | app-db @db/app-db 134 | subs-cache @subs/query->reaction] 135 | (fn [] 136 | ;; call `dispose!` on all current subscriptions which 137 | ;; didn't originally exist. 138 | (let [original-subs (set (vals subs-cache)) 139 | current-subs (set (vals @subs/query->reaction))] 140 | (doseq [sub (set/difference current-subs original-subs)] 141 | (interop/dispose! sub))) 142 | 143 | ;; Reset the atoms 144 | ;; We don't need to reset subs/query->reaction, as 145 | ;; disposing of the subs removes them from the cache anyway 146 | (reset! registrar/kind->id->handler handlers) 147 | (reset! db/app-db app-db) 148 | nil))) 149 | 150 | 151 | ;; -- Event Processing Callbacks 152 | 153 | (defn add-post-event-callback 154 | "Registers a function `f` to be called after each event is processed 155 | `f` will be called with two arguments: 156 | - `event`: a vector. The event just processed. 157 | - `queue`: a PersistentQueue, possibly empty, of events yet to be processed. 158 | 159 | This is useful in advanced cases like: 160 | - you are implementing a complex bootstrap pipeline 161 | - you want to create your own handling infrastructure, with perhaps multiple 162 | handlers for the one event, etc. Hook in here. 163 | - libraries providing 'isomorphic javascript' rendering on Nodejs or Nashorn. 164 | 165 | 'id' is typically a keyword. Supplied at \"add time\" so it can subsequently 166 | be used at \"remove time\" to get rid of the right callback. 167 | " 168 | ([f] 169 | (add-post-event-callback f f)) ;; use f as its own identifier 170 | ([id f] 171 | (router/add-post-event-callback re-frame.router/event-queue id f))) 172 | 173 | 174 | (defn remove-post-event-callback 175 | [id] 176 | (router/remove-post-event-callback re-frame.router/event-queue id)) 177 | 178 | 179 | ;; -- Deprecation Messages 180 | ;; Assisting the v0.0.7 -> v0.0.8 transition. 181 | (defn register-handler 182 | [& args] 183 | (console :warn "re-frame: \"register-handler\" has been renamed \"reg-event-db\" (look for registration of " (str (first args)) ")") 184 | (apply reg-event-db args)) 185 | 186 | (defn register-sub 187 | [& args] 188 | (console :warn "re-frame: \"register-sub\" is deprecated. Use \"reg-sub-raw\" (look for registration of " (str (first args)) ")") 189 | (apply reg-sub-raw args)) 190 | -------------------------------------------------------------------------------- /src/re_frame/db.cljc: -------------------------------------------------------------------------------- 1 | (ns re-frame.db 2 | (:require [re-frame.interop :refer [ratom]])) 3 | 4 | 5 | ;; -- Application State -------------------------------------------------------------------------- 6 | ;; 7 | ;; Should not be accessed directly by application code. 8 | ;; Read access goes through subscriptions. 9 | ;; Updates via event handlers. 10 | (def app-db (ratom {})) 11 | 12 | -------------------------------------------------------------------------------- /src/re_frame/events.cljc: -------------------------------------------------------------------------------- 1 | (ns re-frame.events 2 | (:require [re-frame.db :refer [app-db]] 3 | [re-frame.utils :refer [first-in-vector]] 4 | [re-frame.interop :refer [empty-queue debug-enabled?]] 5 | [re-frame.registrar :refer [get-handler-from-registry register-handler] :as registrar] 6 | [re-frame.loggers :refer [console]] 7 | [re-frame.interceptor :as interceptor] 8 | [re-frame.trace :as trace :include-macros true])) 9 | 10 | 11 | (def kind :event) 12 | (assert (re-frame.registrar/kinds kind)) 13 | 14 | (defn- flatten-and-remove-nils 15 | "`interceptors` might have nested collections, and contain nil elements. 16 | return a flat collection, with all nils removed. 17 | This function is 9/10 about giving good error messages" 18 | [id interceptors] 19 | (let [make-chain #(->> % flatten (remove nil?))] 20 | (if-not debug-enabled? 21 | (make-chain interceptors) 22 | (do ;; do a whole lot of development time checks 23 | (when-not (coll? interceptors) 24 | (console :error (str "re-frame: when registering " id ", expected a collection of interceptors, got:") interceptors)) 25 | (let [chain (make-chain interceptors)] 26 | (when (empty? chain) 27 | (console :error (str "re-frame: when registering" id ", given an empty interceptor chain"))) 28 | (when-let [not-i (first (remove interceptor/interceptor? chain))] 29 | (if (fn? not-i) 30 | (console :error (str "re-frame: when registering " id ", got a function instead of an interceptor. Did you provide old style middleware by mistake? Got:") not-i) 31 | (console :error (str "re-frame: when registering " id ", expected interceptors, but got:") not-i))) 32 | chain))))) 33 | 34 | 35 | (defn register 36 | "Associate the given event `id` with the given collection of `interceptors`. 37 | 38 | `interceptors` may contain nested collections and there may be nils 39 | at any level,so process this structure into a simple, nil-less vector 40 | before registration. 41 | 42 | An `event handler` will likely be at the end of the chain (wrapped in an interceptor)." 43 | [id interceptors] 44 | (register-handler kind id (flatten-and-remove-nils id interceptors))) 45 | 46 | 47 | 48 | ;; -- handle event -------------------------------------------------------------------------------- 49 | 50 | (def ^:dynamic *handling* nil) ;; remember what event we are currently handling 51 | 52 | (defn handle-event-using-registry 53 | "Given an event vector, look up the associated intercepter chain, and execute it." 54 | [registry event-v] 55 | (let [event-id (first-in-vector event-v)] 56 | (if-let [interceptors (get-handler-from-registry registry 57 | kind 58 | event-id 59 | true)] 60 | (if *handling* 61 | (console :error (str "re-frame: while handling \"" *handling* "\", dispatch-sync was called for \"" event-v "\". You can't call dispatch-sync within an event handler.")) 62 | (binding [*handling* event-v] 63 | (trace/with-trace {:operation event-id 64 | :op-type kind 65 | :tags {:event event-v}} 66 | (interceptor/execute event-v interceptors))))))) 67 | 68 | 69 | (defn handle [event-v] 70 | (handle-event-using-registry @registrar/kind->id->handler 71 | event-v)) 72 | -------------------------------------------------------------------------------- /src/re_frame/frank.cljs: -------------------------------------------------------------------------------- 1 | (ns re-frame.frank 2 | (:require [re-frame.utils] 3 | [re-frame.interop :as interop] 4 | [re-frame.router :as router] 5 | [re-frame.registrar :as registrar] 6 | [re-frame.events :as events] 7 | 8 | [re-frame.interceptor :as interceptor] 9 | [re-frame.loggers :as loggers] 10 | [re-frame.trace :as trace])) 11 | 12 | 13 | ;; Adapted from re-frame.registrar/get-handler to take the handler registry 14 | ;; value as parameter 15 | 16 | (defprotocol Frankenstein 17 | (dispatch! [this event-v]) 18 | (dispatch-sync! [this event-v])) 19 | 20 | ;; Inspired by scrum reconciler and re-frame internals 21 | (deftype Frank [registry-atom event-queue state-atom] 22 | Object 23 | (equiv [this other] 24 | (-equiv this other)) 25 | 26 | IAtom 27 | 28 | IMeta 29 | (-meta [_] meta) 30 | 31 | IEquiv 32 | (-equiv [this other] 33 | (identical? this other)) 34 | 35 | IDeref 36 | (-deref [_] 37 | (-deref state-atom)) 38 | 39 | IWatchable 40 | (-add-watch [this key callback] 41 | (add-watch state-atom (list this key) 42 | (fn [_ _ oldv newv] 43 | (when (not= oldv newv) 44 | (callback key this oldv newv)))) 45 | this) 46 | 47 | (-remove-watch [this key] 48 | (remove-watch state-atom (list this key)) 49 | this) 50 | 51 | IHash 52 | (-hash [this] (goog/getUid this)) 53 | 54 | IPrintWithWriter 55 | (-pr-writer [this writer opts] 56 | (-write writer "#object [re-frame.frank.Frank ") 57 | (pr-writer {:val (-deref this)} writer opts) 58 | (-write writer "]")) 59 | 60 | 61 | Frankenstein 62 | (dispatch! [this event-v] 63 | (if (nil? event-v) 64 | (throw (ex-info "re-frankenstein: you called \"dispatch!\" without an event vector." {}))) 65 | (router/push event-queue event-v) 66 | 67 | nil) ;; Ensure nil return. See https://github.com/Day8/re-frame/wiki/Beware-Returning-False 68 | 69 | (dispatch-sync! [this event-v] 70 | (events/handle-event-using-registry @registry-atom event-v) 71 | ;; FIXME when we can use an EventQueue 72 | ;; No post-event-callbacks for now 73 | #_(-call-post-event-callbacks event-queue event-v) ;; slightly ugly hack. Run the registered post event callbacks. 74 | nil)) ;; Ensure nil return 75 | 76 | ;; TODO as this is used somewhere else, maybe create a `utils` namespace? 77 | (defn- map-vals 78 | "Returns a new version of 'm' in which 'f' has been applied to each value. 79 | (map-vals inc {:a 4, :b 2}) => {:a 5, :b 3}" 80 | [f m] 81 | (into (empty m) 82 | (map (fn [[k v]] [k (f v)])) 83 | m)) 84 | 85 | 86 | (defn swap-stateful-interceptors! 87 | "Modifies the registry value by replacing the handlers that refer refer to the 88 | global app-db with very similar handlers that refer to the local-db provided 89 | as argument. Then swap registered stateful interceptors to use new local ones" 90 | 91 | [registry-atom local-db frank] 92 | (let [local-db-coeffect-handler 93 | (fn local-db-coeffect-handler [coeffects] 94 | (assoc coeffects :db @local-db)) 95 | 96 | new-cofx-db-interceptor 97 | (interceptor/->interceptor 98 | :id :coeffects/frank-db 99 | :before (fn coeffects-before [context] 100 | (update context 101 | :coeffects 102 | local-db-coeffect-handler))) 103 | 104 | new-do-fx-interceptor 105 | (interceptor/->interceptor 106 | :id :frank/do-fx 107 | :after (fn do-fx-after [context] 108 | (doseq [[effect-k value] (:effects context)] 109 | (if-let [effect-fn 110 | (registrar/get-handler-from-registry-atom registry-atom 111 | :fx 112 | effect-k 113 | true)] 114 | (effect-fn value 115 | {:dispatch! #(dispatch! frank %) 116 | :dispatch-sync! #(dispatch-sync! frank %)})))))] 117 | 118 | (reset! registry-atom 119 | (-> @registry-atom 120 | (registrar/register-handler-into-registry :cofx 121 | :db 122 | local-db-coeffect-handler) 123 | 124 | (registrar/register-handler-into-registry :fx 125 | :db 126 | (fn [value] (reset! local-db value))) 127 | 128 | (registrar/register-handler-into-registry 129 | :fx :dispatch-later 130 | (fn [value {local-dispatch :dispatch!}] 131 | (doseq [{:keys [ms dispatch] :as effect} value] 132 | (if (or (empty? dispatch) (not (number? ms))) 133 | (loggers/console :error "re-frame: ignoring bad :dispatch-later value:" effect) 134 | (interop/set-timeout! #(local-dispatch dispatch) ms))))) 135 | 136 | (registrar/register-handler-into-registry 137 | :fx :dispatch 138 | (fn [value {local-dispatch :dispatch!}] 139 | (if-not (vector? value) 140 | (loggers/console :error "re-frame: ignoring bad :dispatch value. Expected a vector, but got:" value) 141 | (local-dispatch value)))) 142 | 143 | (registrar/register-handler-into-registry 144 | :fx :dispatch-n 145 | (fn [value {local-dispatch :dispatch!}] 146 | (if-not (sequential? value) 147 | (loggers/console :error "re-frame: ignoring bad :dispatch-n value. Expected a collection, got got:" value) 148 | (doseq [event value] (local-dispatch event))))) 149 | 150 | 151 | (registrar/register-handler-into-registry 152 | :fx :deregister-event-handler 153 | (fn [value] 154 | (let [clear-event (partial registrar/clear-handlers-from-registry-atom registry-atom :fx)] 155 | (doseq [event (if (sequential? value) value [value])] 156 | (clear-event event))))) 157 | 158 | (update :event 159 | (fn [event-handlers-by-id] 160 | (map-vals (fn [interceptors] 161 | (map (fn [interceptor] 162 | (case (:id interceptor) 163 | :coeffects/db new-cofx-db-interceptor 164 | :do-fx new-do-fx-interceptor 165 | interceptor)) 166 | interceptors)) 167 | event-handlers-by-id))))))) 168 | 169 | (defn create 170 | ([] (create (atom {}))) 171 | ([starting-atom] 172 | (let [local-db starting-atom 173 | local-registry-atom (atom @registrar/kind->id->handler) 174 | frank (->Frank local-registry-atom 175 | (router/->EventQueue local-registry-atom 176 | :idle ;; Initial queue state 177 | #queue [] ;; Internal storage for actions 178 | {}) ;; Function to be called after every action 179 | local-db)] 180 | 181 | (swap-stateful-interceptors! local-registry-atom 182 | local-db 183 | frank) 184 | 185 | frank))) 186 | -------------------------------------------------------------------------------- /src/re_frame/fx.cljc: -------------------------------------------------------------------------------- 1 | (ns re-frame.fx 2 | (:require 3 | [re-frame.router :as router] 4 | [re-frame.db :refer [app-db]] 5 | [re-frame.interceptor :refer [->interceptor]] 6 | [re-frame.interop :refer [set-timeout!]] 7 | [re-frame.events :as events] 8 | [re-frame.registrar :refer [get-handler clear-handlers register-handler]] 9 | [re-frame.loggers :refer [console]])) 10 | 11 | 12 | ;; -- Registration ------------------------------------------------------------ 13 | 14 | (def kind :fx) 15 | (assert (re-frame.registrar/kinds kind)) 16 | (def register (partial register-handler kind)) 17 | 18 | ;; -- Interceptor ------------------------------------------------------------- 19 | 20 | (def do-fx 21 | "An interceptor which actions a `context's` (side) `:effects`. 22 | 23 | For each key in the `:effects` map, call the `effects handler` previously 24 | registered using `reg-fx`. 25 | 26 | So, if `:effects` was: 27 | {:dispatch [:hello 42] 28 | :db {...} 29 | :undo \"set flag\"} 30 | call the registered effects handlers for each of the map's keys: 31 | `:dispatch`, `:undo` and `:db`." 32 | (->interceptor 33 | :id :do-fx 34 | :after (fn do-fx-after 35 | [context] 36 | (doseq [[effect-k value] (:effects context)] 37 | (if-let [effect-fn (get-handler kind effect-k true)] 38 | (effect-fn value)))))) 39 | 40 | ;; -- Builtin Effect Handlers ------------------------------------------------ 41 | 42 | ;; :dispatch-later 43 | ;; 44 | ;; `dispatch` one or more events after given delays. Expects a collection 45 | ;; of maps with two keys: :`ms` and `:dispatch` 46 | ;; 47 | ;; usage: 48 | ;; 49 | ;; {:dispatch-later [{:ms 200 :dispatch [:event-id "param"]} ;; in 200ms do this: (dispatch [:event-id "param"]) 50 | ;; {:ms 100 :dispatch [:also :this :in :100ms]}]} 51 | ;; 52 | (register 53 | :dispatch-later 54 | (fn [value] 55 | (doseq [{:keys [ms dispatch] :as effect} value] 56 | (if (or (empty? dispatch) (not (number? ms))) 57 | (console :error "re-frame: ignoring bad :dispatch-later value:" effect) 58 | (set-timeout! #(router/dispatch dispatch) ms))))) 59 | 60 | 61 | ;; :dispatch 62 | ;; 63 | ;; `dispatch` one event. Excepts a single vector. 64 | ;; 65 | ;; usage: 66 | ;; {:dispatch [:event-id "param"] } 67 | 68 | (register 69 | :dispatch 70 | (fn [value] 71 | (if-not (vector? value) 72 | (console :error "re-frame: ignoring bad :dispatch value. Expected a vector, but got:" value) 73 | (router/dispatch value)))) 74 | 75 | 76 | ;; :dispatch-n 77 | ;; 78 | ;; `dispatch` more than one event. Expects a list or vector of events. Something for which 79 | ;; sequential? returns true. 80 | ;; 81 | ;; usage: 82 | ;; {:dispatch-n (list [:do :all] [:three :of] [:these])} 83 | ;; 84 | (register 85 | :dispatch-n 86 | (fn [value] 87 | (if-not (sequential? value) 88 | (console :error "re-frame: ignoring bad :dispatch-n value. Expected a collection, got got:" value)) 89 | (doseq [event value] (router/dispatch event)))) 90 | 91 | 92 | ;; :deregister-event-handler 93 | ;; 94 | ;; removes a previously registered event handler. Expects either a single id ( 95 | ;; typically a keyword), or a seq of ids. 96 | ;; 97 | ;; usage: 98 | ;; {:deregister-event-handler :my-id)} 99 | ;; or: 100 | ;; {:deregister-event-handler [:one-id :another-id]} 101 | ;; 102 | (register 103 | :deregister-event-handler 104 | (fn [value] 105 | (let [clear-event (partial clear-handlers events/kind)] 106 | (if (sequential? value) 107 | (doseq [event (if (sequential? value) value [value])] 108 | (clear-event event)))))) 109 | 110 | 111 | ;; :db 112 | ;; 113 | ;; reset! app-db with a new value. Expects a map. 114 | ;; 115 | ;; usage: 116 | ;; {:db {:key1 value1 key2 value2}} 117 | ;; 118 | (register 119 | :db 120 | (fn [value] 121 | (reset! app-db value))) 122 | 123 | -------------------------------------------------------------------------------- /src/re_frame/interceptor.cljc: -------------------------------------------------------------------------------- 1 | (ns re-frame.interceptor 2 | (:require 3 | [re-frame.loggers :refer [console]] 4 | [re-frame.interop :refer [ratom? empty-queue debug-enabled?]])) 5 | 6 | 7 | (def mandatory-interceptor-keys #{:id :after :before}) 8 | 9 | (defn interceptor? 10 | [m] 11 | (and (map? m) 12 | (= mandatory-interceptor-keys (-> m keys set)))) 13 | 14 | 15 | (defn ->interceptor 16 | "Create an interceptor from named arguments" 17 | [& {:as m :keys [id before after]}] 18 | (when debug-enabled? 19 | (if-let [unknown-keys (seq (clojure.set/difference 20 | (-> m keys set) 21 | mandatory-interceptor-keys))] 22 | (console :error "re-frame: ->interceptor " m " has unknown keys:" unknown-keys))) 23 | {:id (or id :unnamed) 24 | :before before 25 | :after after }) 26 | 27 | ;; -- Effect Helpers ----------------------------------------------------------------------------- 28 | 29 | (defn get-effect 30 | ([context] 31 | (:effects context)) 32 | ([context key] 33 | (get-in context [:effects key])) 34 | ([context key not-found] 35 | (get-in context [:effects key] not-found))) 36 | 37 | 38 | (defn assoc-effect 39 | [context key value] 40 | (assoc-in context [:effects key] value)) 41 | 42 | ;; -- CoEffect Helpers --------------------------------------------------------------------------- 43 | 44 | (defn get-coeffect 45 | ([context] 46 | (:coeffects context)) 47 | ([context key] 48 | (get-in context [:coeffects key])) 49 | ([context key not-found] 50 | (get-in context [:coeffects key] not-found))) 51 | 52 | (defn assoc-coeffect 53 | [context key value] 54 | (assoc-in context [:coeffects key] value)) 55 | 56 | (defn update-coeffect 57 | [context key f & args] 58 | (apply update-in context [:coeffects key] f args)) 59 | 60 | ;; -- Execute Interceptor Chain ------------------------------------------------------------------ 61 | 62 | 63 | (defn- invoke-interceptor-fn 64 | [context interceptor direction] 65 | (if-let [f (get interceptor direction)] 66 | (f context) 67 | context)) 68 | 69 | 70 | (defn- invoke-interceptors 71 | "Loop over all interceptors, calling `direction` function on each, 72 | threading the value of `context` through every call. 73 | 74 | `direction` is one of `:before` or `:after`. 75 | 76 | Each iteration, the next interceptor to process is obtained from 77 | context's `:queue`. After they are processed, interceptors are popped 78 | from `:queue` and added to `:stack`. 79 | 80 | After sufficient iteration, `:queue` will be empty, and `:stack` will 81 | contain all interceptors processed. 82 | 83 | Returns updated `context`. Ie. the `context` which has been threaded 84 | through all interceptor functions. 85 | 86 | Generally speaking, an interceptor's `:before` function will (if present) 87 | add to a `context's` `:coeffects`, while it's `:after` function 88 | will modify the `context`'s `:effects`. Very approximately. 89 | 90 | But because all interceptor functions are given `context`, and can 91 | return a modified version of it, the way is clear for an interceptor 92 | to introspect the stack or queue, or even modify the queue 93 | (add new interceptors via `enqueue`?). This is a very fluid arrangement." 94 | ([context direction] 95 | (loop [context context] 96 | (let [queue (:queue context)] ;; future interceptors 97 | (if (empty? queue) 98 | context 99 | (let [interceptor (peek queue) ;; next interceptor to call 100 | stack (:stack context)] ;; already completed interceptors 101 | 102 | (recur (-> context 103 | (assoc :queue (pop queue) 104 | :stack (conj stack interceptor)) 105 | (invoke-interceptor-fn interceptor direction))))))))) 106 | 107 | 108 | (defn enqueue 109 | "Add a collection of `interceptors` to the end of `context's` execution `:queue`. 110 | Returns the updated `context`. 111 | 112 | In an advanced case, this function could allow an interceptor to add new 113 | interceptors to the `:queue` of a context." 114 | [context interceptors] 115 | (update context :queue 116 | (fnil into empty-queue) 117 | interceptors)) 118 | 119 | 120 | (defn- context 121 | "Create a fresh context" 122 | ([event interceptors] 123 | (-> {} 124 | (assoc-coeffect :event event) 125 | (enqueue interceptors))) 126 | ([event interceptors db] ;; only used in tests, probably a hack, remove ? XXX 127 | (-> (context event interceptors) 128 | (assoc-coeffect :db db)))) 129 | 130 | 131 | (defn- change-direction 132 | "Called on completion of `:before` processing, this function prepares/modifies 133 | `context` for the backwards sweep of processing in which an interceptor 134 | chain's `:after` fns are called. 135 | 136 | At this point in processing, the `:queue` is empty and `:stack` holds all 137 | the previously run interceptors. So this function enables the backwards walk 138 | by priming `:queue` with what's currently in `:stack`" 139 | [context] 140 | (-> context 141 | (dissoc :queue) 142 | (enqueue (:stack context)))) 143 | 144 | 145 | (defn execute 146 | "Executes the given chain (coll) of interceptors. 147 | 148 | Each interceptor has this form: 149 | {:before (fn [context] ...) ;; returns possibly modified context 150 | :after (fn [context] ...)} ;; `identity` would be a noop 151 | 152 | Walks the queue of iterceptors from beginning to end, calling the 153 | `:before` fn on each, then reverse direction and walk backwards, 154 | calling the `:after` fn on each. 155 | 156 | The last interceptor in the chain presumably wraps an event 157 | handler fn. So the overall goal of the process is to \"handle 158 | the given event\". 159 | 160 | Thread a `context` through all calls. `context` has this form: 161 | 162 | {:coeffects {:event [:a-query-id :some-param] 163 | :db } 164 | :effects {:db 165 | :dispatch [:an-event-id :param1]} 166 | :queue 167 | :stack } 168 | 169 | `context` has `:coeffects` and `:effects` which, if this was a web 170 | server, would be somewhat anologous to `request` and `response` 171 | respectively. 172 | 173 | `coeffects` will contain data like `event` and the initial 174 | state of `db` - the inputs required by the event handler 175 | (sitting presumably on the end of the chain), while handler-returned 176 | side effects are put into `:effects` including, but not limited to, 177 | new values for `db`. 178 | 179 | The first few interceptors in a chain will likely have `:before` 180 | functions which \"prime\" the `context` by adding the event, and 181 | the current state of app-db into `:coeffects`. But interceptors can 182 | add whatever they want to `:coeffects` - perhaps the event handler needs 183 | some information from localstore, or a random number, or access to 184 | a DataScript connection. 185 | 186 | Equally, some interceptors in the chain will have `:after` fn 187 | which can process the side effects accumulated into `:effects` 188 | including but, not limited to, updates to app-db. 189 | 190 | Through both stages (before and after), `context` contains a `:queue` 191 | of interceptors yet to be processed, and a `:stack` of interceptors 192 | already done. In advanced cases, these values can be modified by the 193 | functions through which the context is threaded." 194 | [event-v interceptors] 195 | (-> (context event-v interceptors) 196 | (invoke-interceptors :before) 197 | change-direction 198 | (invoke-interceptors :after))) 199 | 200 | -------------------------------------------------------------------------------- /src/re_frame/interop.clj: -------------------------------------------------------------------------------- 1 | (ns re-frame.interop 2 | (:import [java.util.concurrent Executor Executors])) 3 | 4 | 5 | ;; The purpose of this file is to provide JVM-runnable implementations of the 6 | ;; CLJS equivalents in interop.cljs. 7 | ;; 8 | ;; These implementations are to enable you to bring up a re-frame app on the JVM 9 | ;; in order to run tests, or to develop at a JVM REPL instead of a CLJS one. 10 | ;; 11 | ;; Please note, though, that the purpose here *isn't* to fully replicate all of 12 | ;; re-frame's behaviour in a real CLJS environment. We don't have Reagent or 13 | ;; React on the JVM, and we don't try to mimic the stateful lifecycles that they 14 | ;; embody. 15 | ;; 16 | ;; In particular, if you're performing side effects in any code that's triggered 17 | ;; by a change to a Ratom's value, and not via a call to `dispatch`, then you're 18 | ;; going to have a hard time getting any accurate tests with this code. 19 | ;; However, if your subscriptions and Reagent render functions are pure, and 20 | ;; your side-effects are all managed by effect handlers, then hopefully this will 21 | ;; allow you to write some useful tests that can run on the JVM. 22 | 23 | 24 | (defonce ^:private executor (Executors/newSingleThreadExecutor)) 25 | 26 | (defn next-tick [f] 27 | (let [bound-f (bound-fn [& args] (apply f args))] 28 | (.execute ^Executor executor bound-f)) 29 | nil) 30 | 31 | (def empty-queue clojure.lang.PersistentQueue/EMPTY) 32 | 33 | (def after-render next-tick) 34 | 35 | (def debug-enabled? true) 36 | 37 | (defn ratom [x] 38 | (atom x)) 39 | 40 | (defn ratom? [x] 41 | (instance? clojure.lang.IAtom x)) 42 | 43 | (defn deref? [x] 44 | (instance? clojure.lang.IDeref x)) 45 | 46 | (defn make-reaction 47 | "On JVM Clojure, return a `deref`-able thing which invokes the given function 48 | on every `deref`. That is, `make-reaction` here provides precisely none of the 49 | benefits of `reagent.ratom/make-reaction` (which only invokes its function if 50 | the reactions that the function derefs have changed value). But so long as `f` 51 | only depends on other reactions (which also behave themselves), the only 52 | difference is one of efficiency. That is, your tests should see no difference 53 | other than that they do redundant work." 54 | [f] 55 | (reify clojure.lang.IDeref 56 | (deref [_] (f)))) 57 | 58 | (defn add-on-dispose! 59 | "No-op in JVM Clojure, since for testing purposes, we don't care about 60 | releasing resources for efficiency purposes." 61 | [a-ratom f] 62 | nil) 63 | 64 | (defn dispose! [a-ratom] 65 | "No-op in JVM Clojure, since for testing purposes, we don't care about 66 | releasing resources for efficiency purposes." 67 | nil) 68 | 69 | (defn set-timeout! 70 | "Note that we ignore the `ms` value and just invoke the function, because 71 | there isn't often much point firing a timed event in a test." 72 | [f ms] 73 | (next-tick f)) 74 | 75 | (defn now [] 76 | ;; currentTimeMillis may count backwards in some scenarios, but as this is used for tracing 77 | ;; it is preferable to the slower but more accurate System.nanoTime. 78 | (System/currentTimeMillis)) 79 | 80 | (defn reagent-id 81 | "Doesn't make sense in a Clojure context currently." 82 | [reactive-val] 83 | nil) 84 | -------------------------------------------------------------------------------- /src/re_frame/interop.cljs: -------------------------------------------------------------------------------- 1 | (ns re-frame.interop 2 | (:require [goog.async.nextTick] 3 | [reagent.core] 4 | [reagent.ratom])) 5 | 6 | (def next-tick goog.async.nextTick) 7 | 8 | (def empty-queue #queue []) 9 | 10 | (def after-render reagent.core/after-render) 11 | 12 | ;; Make sure the Google Closure compiler sees this as a boolean constant, 13 | ;; otherwise Dead Code Elimination won't happen in `:advanced` builds. 14 | ;; Type hints have been liberally sprinkled. 15 | ;; https://developers.google.com/closure/compiler/docs/js-for-compiler 16 | (def ^boolean debug-enabled? "@define {boolean}" ^boolean js/goog.DEBUG) 17 | 18 | (defn ratom [x] 19 | (reagent.core/atom x)) 20 | 21 | (defn ratom? [x] 22 | (satisfies? reagent.ratom/IReactiveAtom x)) 23 | 24 | (defn deref? [x] 25 | (satisfies? IDeref x)) 26 | 27 | 28 | (defn make-reaction [f] 29 | (reagent.ratom/make-reaction f)) 30 | 31 | (defn add-on-dispose! [a-ratom f] 32 | (reagent.ratom/add-on-dispose! a-ratom f)) 33 | 34 | (defn dispose! [a-ratom] 35 | (reagent.ratom/dispose! a-ratom)) 36 | 37 | (defn set-timeout! [f ms] 38 | (js/setTimeout f ms)) 39 | 40 | (defn now [] 41 | (if (exists? js/performance.now) 42 | (js/performance.now) 43 | (js/Date.now))) 44 | 45 | (defn reagent-id 46 | "Produces an id for reactive Reagent values 47 | e.g. reactions, ratoms, cursors." 48 | [reactive-val] 49 | (when (implements? reagent.ratom/IReactiveAtom reactive-val) 50 | (str (condp instance? reactive-val 51 | reagent.ratom/RAtom "ra" 52 | reagent.ratom/RCursor "rc" 53 | reagent.ratom/Reaction "rx" 54 | reagent.ratom/Track "tr" 55 | "other") 56 | (hash reactive-val)))) 57 | -------------------------------------------------------------------------------- /src/re_frame/loggers.cljc: -------------------------------------------------------------------------------- 1 | (ns re-frame.loggers 2 | (:require 3 | [clojure.set :refer [difference]] 4 | #?@(:clj [[clojure.string :as str] 5 | [clojure.tools.logging :as log]]))) 6 | 7 | #?(:clj (defn log [level & args] 8 | (log/log level (if (= 1 (count args)) 9 | (first args) 10 | (str/join " " args))))) 11 | 12 | 13 | ;; XXX should loggers be put in the registrar ?? 14 | (def ^:private loggers 15 | "Holds the current set of logging functions. 16 | By default, re-frame uses the functions provided by js/console. 17 | Use `set-loggers!` to change these defaults 18 | " 19 | (atom #?(:cljs {:log (js/console.log.bind js/console) 20 | :warn (js/console.warn.bind js/console) 21 | :error (js/console.error.bind js/console) 22 | :group (if (.-group js/console) ;; console.group does not exist < IE 11 23 | (js/console.group.bind js/console) 24 | (js/console.log.bind js/console)) 25 | :groupEnd (if (.-groupEnd js/console) ;; console.groupEnd does not exist < IE 11 26 | (js/console.groupEnd.bind js/console) 27 | #())}) 28 | ;; clojure versions 29 | #?(:clj {:log (partial log :info) 30 | :warn (partial log :warn) 31 | :error (partial log :error) 32 | :group (partial log :info) 33 | :groupEnd #()}))) 34 | 35 | (defn console 36 | [level & args] 37 | (assert (contains? @loggers level) (str "re-frame: log called with unknown level: " level)) 38 | (apply (level @loggers) args)) 39 | 40 | 41 | (defn set-loggers! 42 | "Change the set (or a subset) of logging functions used by re-frame. 43 | `new-loggers` should be a map with the same keys as `loggers` (above)" 44 | [new-loggers] 45 | (assert (empty? (difference (set (keys new-loggers)) (-> @loggers keys set))) "Unknown keys in new-loggers") 46 | (swap! loggers merge new-loggers)) 47 | 48 | (defn get-loggers 49 | "Get the current logging functions used by re-frame." 50 | [] 51 | @loggers) 52 | -------------------------------------------------------------------------------- /src/re_frame/registrar.cljc: -------------------------------------------------------------------------------- 1 | (ns re-frame.registrar 2 | "In many places, re-frame asks you to associate an `id` (keyword) 3 | with a `handler` (function). This namespace contains the 4 | central registry of such associations." 5 | (:require [re-frame.interop :refer [debug-enabled?]] 6 | [re-frame.loggers :refer [console]])) 7 | 8 | 9 | ;; kinds of handlers 10 | (def kinds #{:event :fx :cofx :sub}) 11 | 12 | ;; This atom contains a register of all handlers. 13 | ;; Contains a map keyed first by `kind` (of handler), and then `id`. 14 | ;; Leaf nodes are handlers. 15 | (def kind->id->handler (atom {})) 16 | 17 | (defn get-handler-from-registry 18 | ([registry kind] 19 | (get registry kind)) 20 | 21 | ([registry kind id] 22 | (-> (get registry kind) 23 | (get id))) 24 | 25 | ([registry kind id required?] 26 | (let [handler (get-handler-from-registry registry kind id)] 27 | (when debug-enabled? ;; This is in a separate when so Closure DCE can run 28 | (when (and required? (nil? handler)) ;; Otherwise you'd need to type hint the and with a ^boolean for DCE. 29 | (console :error "re-frame: no " (str kind) " handler registered for:" id))) 30 | handler))) 31 | 32 | (defn get-handler-from-registry-atom [registry-atom & args] 33 | (apply get-handler-from-registry @registry-atom args)) 34 | 35 | (defn get-handler [& args] 36 | (apply get-handler-from-registry @kind->id->handler args)) 37 | 38 | 39 | (defn register-handler-into-registry [registry kind id handler-fn] 40 | (assoc-in registry [kind id] handler-fn)) 41 | 42 | (defn register-handler [kind id handler-fn] 43 | (when debug-enabled? ;; This is in a separate when so Closure DCE can run 44 | (when (get-handler kind id false) 45 | (console :warn "re-frame: overwriting" (str kind) "handler for:" id))) ;; allow it, but warn. Happens on figwheel reloads. 46 | 47 | (swap! kind->id->handler register-handler-into-registry kind id handler-fn) 48 | handler-fn) ;; note: returns the just registered handler 49 | 50 | 51 | 52 | (defn clear-handlers-from-registry 53 | ([registry] {}) ;; clear all kinds 54 | 55 | ([registry kind] ;; clear all handlers for this kind 56 | (assert (kinds kind)) 57 | (dissoc registry kind)) 58 | 59 | ([registry kind id] ;; clear a single handler for a kind 60 | (assert (kinds kind)) 61 | (if (get-handler-from-registry registry kind id) 62 | (update-in registry [kind] dissoc id) 63 | (do (console :warn "re-frame: can't clear" (str kind) "handler for" (str id ". Handler not found.")) 64 | registry)))) 65 | 66 | 67 | (defn clear-handlers-from-registry-atom [registry-atom & args] 68 | (reset! registry-atom 69 | (apply clear-handlers-from-registry @registry-atom args))) 70 | 71 | (defn clear-handlers [& args] 72 | (reset! kind->id->handler 73 | (apply clear-handlers-from-registry @kind->id->handler args))) 74 | -------------------------------------------------------------------------------- /src/re_frame/rum.cljs: -------------------------------------------------------------------------------- 1 | (ns re-frame.rum 2 | (:require [goog.object :as gobj] 3 | [re-frame.frank :as frank])) 4 | 5 | 6 | (defn wrap-dispatch [dispatch-fn frank-instance] 7 | (fn [event args] 8 | (dispatch-fn frank-instance event args))) 9 | 10 | ;; Surprisingly, trying to require react in the let just beside failed silently... 11 | (def ^:private React (cond (exists? js/React) js/React 12 | (exists? js/require) (js/require "react") 13 | :else (throw "Did you forget to include react?"))) 14 | 15 | (def ^:private PropTypes (cond (exists? js/PropTypes) js/PropTypes 16 | (exists? (gobj/get React "PropTypes")) (gobj/get React "PropTypes") 17 | (exists? js/require) (js/require "prop-types") 18 | :else (throw "Did you forget to include prop-types?"))) 19 | 20 | (let [frank-k "re-frame/frank" 21 | context-types {frank-k PropTypes.object}] 22 | 23 | (defn inject-frank-into-context 24 | "Inject Frank instance into current react context. The Frank instance 25 | must be provided as argument to the component to which this mixins is applied. 26 | The `get-frank-fn` will then be used to extract the Frank from the 27 | arguments." 28 | [get-frank-fn] 29 | {:init (fn [rum-state _] 30 | (assoc rum-state ::frank (get-frank-fn (:rum/args rum-state)))) 31 | :class-properties {:childContextTypes context-types} 32 | 33 | ;; Pending issue https://github.com/tonsky/rum/pull/137 34 | :child-context-types context-types 35 | 36 | :child-context (fn [rum-state] {frank-k (::frank rum-state)})}) 37 | 38 | (def with-frank 39 | {:class-properties {:contextTypes context-types} 40 | 41 | ;; Pending issue https://github.com/tonsky/rum/pull/137 42 | :context-types context-types 43 | 44 | :will-mount (fn [rum-state] 45 | (let [frank (-> (:rum/react-component rum-state) 46 | (gobj/get "context") 47 | (gobj/get frank-k))] 48 | (assoc rum-state 49 | :frank/frank frank 50 | :frank/dispatch! (wrap-dispatch frank/dispatch! frank) 51 | :frank/dispatch-sync! (wrap-dispatch frank/dispatch-sync! frank)))) 52 | :will-unmount (fn [rum-state] (dissoc rum-state 53 | :frank/frank 54 | :frank/dispatch! 55 | :frank/dispatch-sync!))}) 56 | 57 | ;;;;;;;;;;;;;;;;;;;;;;;;;; 58 | ;; TEMPORARY WORKAROUND ;; 59 | ;;;;;;;;;;;;;;;;;;;;;;;;;; 60 | 61 | ;; Use these mixins if you want to use both `re-frame.frank` 62 | ;; `org org.martinklepsch.derivatives`. 63 | 64 | ;; This is to make code a bit more concise to declare react contextTypes and 65 | ;; childContextTypes. Hopefully rum will soon improve its handling of react 66 | ;; context types. 67 | 68 | (let [aggregated-context-types (assoc context-types 69 | "org.martinklepsch.derivatives/get" PropTypes.func 70 | "org.martinklepsch.derivatives/release" PropTypes.func)] 71 | 72 | (def drv+frank-ctx {:class-properties {:contextTypes aggregated-context-types}}) 73 | (def drv+frank-child-ctx {:class-properties {:childContextTypes aggregated-context-types}}))) 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /src/re_frame/trace.cljc: -------------------------------------------------------------------------------- 1 | (ns re-frame.trace 2 | "Tracing for re-frame. 3 | Alpha quality, subject to change/break at any time." 4 | #?(:cljs (:require-macros [net.cgrand.macrovich :as macros] 5 | [re-frame.trace :refer [finish-trace with-trace merge-trace!]])) 6 | (:require [re-frame.interop :as interop] 7 | #?(:clj [net.cgrand.macrovich :as macros]) 8 | [re-frame.loggers :refer [console]])) 9 | 10 | (def id (atom 0)) 11 | (def ^:dynamic *current-trace* nil) 12 | 13 | (defn reset-tracing! [] 14 | (reset! id 0)) 15 | 16 | #?(:cljs (goog-define trace-enabled? false) 17 | :clj (def ^boolean trace-enabled? false)) 18 | 19 | (defn ^boolean is-trace-enabled? 20 | "See https://groups.google.com/d/msg/clojurescript/jk43kmYiMhA/IHglVr_TPdgJ for more details" 21 | [] 22 | trace-enabled?) 23 | 24 | (def trace-cbs (atom {})) 25 | 26 | (defn register-trace-cb 27 | "Registers a tracing callback function which will receive a collection of one or more traces. 28 | Will replace an existing callback function if it shares the same key." 29 | [key f] 30 | (swap! trace-cbs assoc key f)) 31 | 32 | (defn remove-trace-cb [key] 33 | (swap! trace-cbs dissoc key) 34 | nil) 35 | 36 | (defn next-id [] (swap! id inc)) 37 | 38 | (defn start-trace [{:keys [operation op-type tags child-of]}] 39 | {:id (next-id) 40 | :operation operation 41 | :op-type op-type 42 | :tags tags 43 | :child-of (or child-of (:id *current-trace*)) 44 | :start (interop/now)}) 45 | 46 | (macros/deftime 47 | (defmacro finish-trace [trace] 48 | `(when (is-trace-enabled?) 49 | (let [end# (interop/now) 50 | duration# (- end# (:start ~trace))] 51 | (doseq [[k# cb#] @trace-cbs] 52 | (try (cb# [(assoc ~trace 53 | :duration duration# 54 | :end (interop/now))]) 55 | #?(:clj (catch Exception e# 56 | (console :error "Error thrown from trace cb" k# "while storing" ~trace e#))) 57 | #?(:cljs (catch :default e# 58 | (console :error "Error thrown from trace cb" k# "while storing" ~trace e#)))))))) 59 | 60 | (defmacro with-trace 61 | "Create a trace inside the scope of the with-trace macro 62 | 63 | Common keys for trace-opts 64 | :op-type - what kind of operation is this? e.g. :sub/create, :render. 65 | :operation - identifier for the operation, for an subscription it would be the subscription keyword 66 | tags - a map of arbitrary kv pairs" 67 | [{:keys [operation op-type tags child-of] :as trace-opts} & body] 68 | `(if (is-trace-enabled?) 69 | (binding [*current-trace* (start-trace ~trace-opts)] 70 | (try ~@body 71 | (finally (finish-trace *current-trace*)))) 72 | (do ~@body))) 73 | 74 | (defmacro merge-trace! [m] 75 | ;; Overwrite keys in tags, and all top level keys. 76 | `(when (is-trace-enabled?) 77 | (let [new-trace# (-> (update *current-trace* :tags merge (:tags ~m)) 78 | (merge (dissoc ~m :tags)))] 79 | (set! *current-trace* new-trace#)) 80 | nil))) 81 | -------------------------------------------------------------------------------- /src/re_frame/utils.cljc: -------------------------------------------------------------------------------- 1 | (ns re-frame.utils 2 | (:require 3 | [re-frame.loggers :refer [console]])) 4 | 5 | (defn dissoc-in 6 | "Dissociates an entry from a nested associative structure returning a new 7 | nested structure. keys is a sequence of keys. Any empty maps that result 8 | will not be present in the new structure. 9 | The key thing is that 'm' remains identical? to istelf if the path was never present" 10 | [m [k & ks :as keys]] 11 | (if ks 12 | (if-let [nextmap (get m k)] 13 | (let [newmap (dissoc-in nextmap ks)] 14 | (if (seq newmap) 15 | (assoc m k newmap) 16 | (dissoc m k))) 17 | m) 18 | (dissoc m k))) 19 | 20 | (defn first-in-vector 21 | [v] 22 | (if (vector? v) 23 | (first v) 24 | (console :error "re-frame: expected a vector, but got:" v))) 25 | -------------------------------------------------------------------------------- /test/re-frame/event_test.cljs: -------------------------------------------------------------------------------- 1 | (ns re-frame.event-test 2 | (:require [cljs.test :refer-macros [is deftest]] 3 | [re-frame.db :as db] 4 | [re-frame.core :as re-frame])) 5 | 6 | ;=====test basic subscriptions ====== 7 | 8 | ;; disabled as it doesn't really test anything 9 | #_(deftest test-event-def 10 | "tests that an error thrown generates an informational warning" 11 | (re-frame/clear-all-events!) 12 | 13 | (re-frame/reg-event-db 14 | :test-event 15 | (fn [db [event-kw stack]] 16 | (throw (js/Error. "thrown in handler")) 17 | db)) 18 | 19 | (defn test-fn1 20 | [] 21 | (re-frame/dispatch [:test-event])) 22 | 23 | (defn test-fn2 24 | [] 25 | (test-fn1)) 26 | 27 | (test-fn2) 28 | ) 29 | 30 | 31 | -------------------------------------------------------------------------------- /test/re-frame/frank_test.cljs: -------------------------------------------------------------------------------- 1 | (ns re-frame.frank-test 2 | (:require [cljs.test :refer-macros [is deftest async use-fixtures testing]] 3 | [re-frame.core :as re-frame] 4 | [re-frame.db] 5 | [re-frame.std-interceptors :as std-interceptors] 6 | [re-frame.registrar :as registrar] 7 | [re-frame.frank :as frank])) 8 | 9 | ;; ---- FIXTURES --------------------------------------------------------------- 10 | 11 | ;; This fixture uses the re-frame.core/make-restore-fn to checkpoint and reset 12 | ;; to cleanup any dynamically registered handlers from our tests. 13 | (defn fixture-re-frame 14 | [] 15 | (let [restore-re-frame (atom nil)] 16 | {:before (fn [] 17 | (reset! restore-re-frame (re-frame.core/make-restore-fn)) 18 | (reset! re-frame.db/app-db {}) 19 | (registrar/clear-handlers)) 20 | :after #(@restore-re-frame)})) 21 | 22 | 23 | (use-fixtures :each (fixture-re-frame)) 24 | 25 | 26 | ;; ---- TESTS ------------------------------------------------------------------ 27 | 28 | (deftest empty-frank 29 | (testing "create works" 30 | (is (frank/create))) 31 | 32 | (testing "default value is an empty map" 33 | (let [frank (frank/create)] 34 | (is (= {} @frank))))) 35 | 36 | (deftest synchronous 37 | (testing "re-frame.db/app-db is empty" 38 | (is (= {} @re-frame.db/app-db))) 39 | ;; check that we can write in the db 40 | (re-frame/reg-event-db ::insert-plop (fn [db [_ message]] (assoc db :plop message))) 41 | ;;(is (frank/create)) 42 | (testing "dispatch and mutate only our new monster" 43 | (let [frank (frank/create)] 44 | (frank/dispatch-sync! frank [::insert-plop "plop"]) 45 | (is (= {:plop "plop"} @frank)) 46 | (is (= {} @re-frame.db/app-db)))) 47 | 48 | ;; check that we can read what we wrote in the db 49 | (re-frame/reg-event-db 50 | ::plop-after-plop 51 | (fn [db [_ plop]] (update db :plop clojure.string/upper-case))) 52 | 53 | 54 | (testing "we can use the mutated local state in further transactions " 55 | (let [frank (frank/create)] 56 | (frank/dispatch-sync! frank [::insert-plop "plop"]) 57 | (frank/dispatch-sync! frank [::plop-after-plop]) 58 | (is (= {:plop "PLOP"} @frank))))) 59 | 60 | (deftest asynchronous 61 | (re-frame/reg-event-db ::insert-plop (fn [db [_ message]] (assoc db :plop message))) 62 | (re-frame/reg-event-db 63 | ::plop-after-plop 64 | (fn [db [_ plop]] (update db :plop clojure.string/upper-case))) 65 | 66 | (testing "using the internal eventQueue" 67 | (let [frank (frank/create)] 68 | (frank/dispatch! frank [::insert-plop "plop"]) 69 | (frank/dispatch! frank [::plop-after-plop]) 70 | 71 | (async done 72 | (js/setTimeout (fn [] 73 | (is (= {:plop "PLOP"} @frank)) 74 | (done)) 75 | 100))))) 76 | 77 | 78 | (deftest with-interceptors 79 | (re-frame/reg-event-db ::insert-plop 80 | [std-interceptors/trim-v 81 | (std-interceptors/path :plop)] 82 | (fn [db [message]] message)) 83 | 84 | (testing "interceptors work as usual" 85 | (let [frank (frank/create)] 86 | (frank/dispatch-sync! frank [::insert-plop "plouf"]) 87 | (is (= {:plop "plouf"} @frank))))) 88 | 89 | (deftest with-standard-effects 90 | (re-frame/reg-event-fx ::trigger-some-standard-effects 91 | (fn [_ _] 92 | {:db {:plop 0} 93 | :dispatch [::inc] 94 | :dispatch-later [{:ms 5 :dispatch [::inc]} 95 | {:ms 5 :dispatch [::inc]}] 96 | :dispatch-n [[::inc] 97 | [::inc]]})) 98 | (re-frame/reg-event-db ::inc 99 | [(std-interceptors/path :plop)] 100 | (fn [val _] (inc val))) 101 | 102 | (let [frank (frank/create)] 103 | (frank/dispatch-sync! frank [::trigger-some-standard-effects]) 104 | (is (= {:plop 0} @frank)) 105 | 106 | (async done 107 | (js/setTimeout (fn [] 108 | (is (= {:plop 5} @frank)) 109 | (done)) 110 | 10)))) 111 | 112 | (deftest with-custom-effect 113 | (re-frame/reg-event-fx ::trigger-side-plop 114 | (fn [_ _] 115 | {:db {:plop "step 1"} 116 | ::side-plop {:some-key "step 2"}})) 117 | (re-frame/reg-fx ::side-plop 118 | (fn [{value :some-key} {:keys [dispatch!]}] 119 | (dispatch! [::side-plop-success value]))) 120 | (re-frame/reg-event-db ::side-plop-success 121 | [(std-interceptors/path :plop) 122 | std-interceptors/trim-v] 123 | (fn [_ [value]] value)) 124 | (let [frank (frank/create)] 125 | (frank/dispatch-sync! frank [::trigger-side-plop]) 126 | (is (= {:plop "step 1"} @frank)) 127 | 128 | (async done 129 | (js/setTimeout (fn [] 130 | (is (= {:plop "step 2"} @frank)) 131 | (done)) 132 | 100)))) 133 | 134 | 135 | (deftest with-starting-atom 136 | (re-frame/reg-event-db ::inc 137 | [(std-interceptors/path :plop)] 138 | (fn [val _] (inc val))) 139 | (let [starting-atom (atom {:plop 1}) 140 | frank (frank/create starting-atom)] 141 | (frank/dispatch-sync! frank [::inc]) 142 | (is (= {:plop 2} @frank)))) 143 | -------------------------------------------------------------------------------- /test/re-frame/fx_test.cljs: -------------------------------------------------------------------------------- 1 | (ns re-frame.fx-test 2 | (:require 3 | [cljs.test :refer-macros [is deftest async use-fixtures]] 4 | [re-frame.core :as re-frame] 5 | [re-frame.fx] 6 | [re-frame.interop :refer [set-timeout!]] 7 | [re-frame.loggers :as log] 8 | [clojure.string :as str])) 9 | 10 | ;; ---- FIXTURES --------------------------------------------------------------- 11 | 12 | ;; This fixture uses the re-frame.core/make-restore-fn to checkpoint and reset 13 | ;; to cleanup any dynamically registered handlers from our tests. 14 | (defn fixture-re-frame 15 | [] 16 | (let [restore-re-frame (atom nil)] 17 | {:before #(reset! restore-re-frame (re-frame.core/make-restore-fn)) 18 | :after #(@restore-re-frame)})) 19 | 20 | (use-fixtures :each (fixture-re-frame)) 21 | 22 | ;; ---- TESTS ------------------------------------------------------------------ 23 | 24 | (deftest dispatch-later 25 | (let [seen-events (atom [])] 26 | ;; Setup and excercise effects handler with :dispatch-later. 27 | (re-frame/reg-event-fx 28 | ::later-test 29 | (fn [_world _event-v] 30 | (re-frame/reg-event-db 31 | ::watcher 32 | (fn [db [_ token]] 33 | (is (#{:event1 :event2 :event3} token) "unexpected: token passed through") 34 | (swap! seen-events #(conj % token)) 35 | db)) 36 | {:dispatch-later [{:ms 100 :dispatch [::watcher :event1]} 37 | {:ms 200 :dispatch [::watcher :event2]} 38 | {:ms 200 :dispatch [::watcher :event3]}]})) 39 | 40 | (async done 41 | (set-timeout! 42 | (fn [] 43 | (is (= @seen-events [:event1 :event2 :event3]) "All 3 events should have fired in order") 44 | (done)) 45 | 1000) 46 | ;; kick off main handler 47 | (re-frame/dispatch [::later-test])))) 48 | 49 | (re-frame/reg-event-fx 50 | ::missing-handler-test 51 | (fn [_world _event-v] 52 | {:fx-not-exist [:nothing :here]})) 53 | 54 | (deftest report-missing-handler 55 | (let [logs (atom []) 56 | log-fn (fn [& args] (swap! logs conj (str/join args))) 57 | original-loggers (log/get-loggers)] 58 | (try 59 | (log/set-loggers! {:error log-fn}) 60 | (re-frame/dispatch-sync [::missing-handler-test]) 61 | (is (re-matches #"re-frame: no :fx handler registered for::fx-not-exist" (first @logs))) 62 | (is (= (count @logs) 1)) 63 | (finally 64 | (log/set-loggers! original-loggers))))) 65 | -------------------------------------------------------------------------------- /test/re-frame/interceptor_test.cljs: -------------------------------------------------------------------------------- 1 | (ns re-frame.interceptor-test 2 | (:require [cljs.test :refer-macros [is deftest testing]] 3 | [reagent.ratom :refer [atom]] 4 | [re-frame.interceptor :refer [context get-coeffect assoc-effect assoc-coeffect get-effect update-coeffect]] 5 | [re-frame.std-interceptors :refer [debug trim-v path enrich after on-changes 6 | db-handler->interceptor fx-handler->interceptor]] 7 | [re-frame.interceptor :as interceptor])) 8 | 9 | (enable-console-print!) 10 | 11 | (deftest test-trim-v 12 | (let [ctx (context [:event-id :b :c] []) 13 | ctx-trimmed ((:before trim-v) ctx) 14 | ctx-untrimmed ((:after trim-v) ctx-trimmed)] 15 | (is (= (get-coeffect ctx-trimmed :event) 16 | [:b :c])) 17 | (is (= (get-coeffect ctx-untrimmed :event) 18 | [:event-id :b :c])) 19 | (is (= ctx-untrimmed ctx)))) 20 | 21 | 22 | (deftest test-one-level-path 23 | (let [db {:showing true :another 1} 24 | p1 (path [:showing])] ;; a simple one level path 25 | 26 | (let [b4 (-> (context [] [] db) 27 | ((:before p1))) ;; before 28 | a (-> b4 29 | (assoc-effect :db false) 30 | ((:after p1)))] ;; after 31 | 32 | (is (= (get-coeffect b4 :db) ;; test before 33 | true)) 34 | (is (= (get-effect a :db) ;; test after 35 | {:showing false :another 1}))))) 36 | 37 | 38 | (deftest test-two-level-path 39 | (let [db {:1 {:2 :target}} 40 | p (path [:1 :2])] ;; a two level path 41 | 42 | (let [b4 (-> (context [] [] db) 43 | ((:before p))) ] ;; before 44 | 45 | (is (= (get-coeffect b4 :db)) ;; test before 46 | :target) 47 | 48 | ;; test #1 49 | (is (= {:1 {:2 :4}} 50 | (-> b4 51 | (assoc-effect :db :4) ;; <-- db becomes :4 52 | ((:after p)) 53 | (get-effect :db)))) 54 | 55 | ;; test #2 - set db to nil 56 | (is (= {:1 {:2 nil}} 57 | (-> b4 58 | (assoc-effect :db nil) ;; <-- db becomes nil 59 | ((:after p)) 60 | (get-effect :db))))))) 61 | 62 | (deftest path-with-no-db-returned 63 | (let [path-interceptor (path :a)] 64 | (-> (context [] [path-interceptor] {:a 1}) 65 | (interceptor/invoke-interceptors :before) 66 | interceptor/change-direction 67 | (interceptor/invoke-interceptors :after) 68 | (get-effect :db) 69 | (nil?) ;; We don't expect an effect to be added. 70 | (is)))) 71 | 72 | (deftest test-db-handler-interceptor 73 | (let [event [:a :b] 74 | 75 | handler (fn [db v] 76 | ;; make sure it was given the right arguements 77 | (is (= db :original-db-val)) 78 | (is (= v event)) 79 | ;; return a specific value for later checking 80 | :new-db-val) 81 | 82 | i1 (db-handler->interceptor handler) 83 | db (-> (context event [] :original-db-val) 84 | ((:before i1)) ;; calls handler - causing :db in :effects to change 85 | (get-effect :db))] 86 | (is (= db :new-db-val)))) 87 | 88 | 89 | 90 | (deftest test-fx-handler-interceptor 91 | (let [event [:a :b] 92 | coeffect {:db 4 :event event} 93 | effect {:db 5 :dispatch [:a]} 94 | 95 | handler (fn [world v] 96 | ;; make sure it was given the right arguements 97 | (is (= world coeffect)) 98 | (is (= v event)) 99 | 100 | ;; return a specific value for later checking 101 | effect) 102 | 103 | i1 (fx-handler->interceptor handler) 104 | e (-> (context event [] (:db coeffect)) 105 | ((:before i1)) ;; call the handler 106 | (get-effect))] 107 | (is (= e {:db 5 :dispatch [:a]})))) 108 | 109 | 110 | 111 | (deftest test-on-changes 112 | (let [change-handler-i (-> (fn [db v] (assoc db :a 10)) 113 | db-handler->interceptor) 114 | 115 | no-change-handler-i (-> (fn [db v] db) 116 | db-handler->interceptor) 117 | 118 | change-i (on-changes + [:c] [:a] [:b]) 119 | orig-db {:a 0 :b 2}] 120 | 121 | (is (= {:a 0 :b 2} 122 | (-> (context [] [] orig-db) 123 | ((:before no-change-handler-i)) ;; no change to :a and :b 124 | ((:after change-i)) 125 | (get-effect :db)))) 126 | (is (= {:a 10 :b 2 :c 12} 127 | (-> (context [] [] orig-db) 128 | ((:before change-handler-i)) ;; cause change to :a 129 | ((:after change-i)) 130 | (get-effect :db)))))) 131 | 132 | (deftest test-after 133 | (testing "when no db effect is returned" 134 | (let [after-db-val (atom nil)] 135 | (-> (context [:a :b] 136 | [(after (fn [db] (reset! after-db-val db)))] 137 | {:a 1}) 138 | (interceptor/invoke-interceptors :before) 139 | interceptor/change-direction 140 | (interceptor/invoke-interceptors :after)) 141 | (is (= @after-db-val {:a 1}))))) 142 | 143 | (deftest test-enrich 144 | (testing "when no db effect is returned" 145 | (let [ctx (context [] [] {:a 1})] 146 | (is (= ::not-found (get-effect ctx :db ::not-found))) 147 | (-> ctx (:after (enrich (fn [db] (is (= db {:a 1}))))))))) 148 | 149 | (deftest test-update-coeffect 150 | (let [context {:effects {:db {:a 1}} 151 | :coeffects {:db {:a 1}}}] 152 | (is (= {:effects {:db {:a 1}} 153 | :coeffects {:db {:a 2}}} 154 | (update-coeffect context :db update :a inc))))) 155 | -------------------------------------------------------------------------------- /test/re-frame/restore_test.cljs: -------------------------------------------------------------------------------- 1 | (ns re-frame.restore-test 2 | (:require [cljs.test :refer-macros [is deftest async use-fixtures testing]] 3 | [re-frame.core :refer [make-restore-fn reg-sub subscribe]] 4 | [re-frame.subs :as subs])) 5 | 6 | ;; TODO: future tests in this area could check DB state and registrations are being correctly restored. 7 | 8 | (use-fixtures :each {:before subs/clear-all-handlers!}) 9 | 10 | (defn one? [x] (= 1 x)) 11 | (defn two? [x] (= 2 x)) 12 | 13 | (defn register-test-subs [] 14 | (reg-sub 15 | :test-sub 16 | (fn [db ev] 17 | (:test-sub db))) 18 | 19 | (reg-sub 20 | :test-sub2 21 | (fn [db ev] 22 | (:test-sub2 db)))) 23 | 24 | (deftest make-restore-fn-test 25 | (testing "no existing subs, then making one subscription" 26 | (register-test-subs) 27 | (let [original-subs @subs/query->reaction 28 | restore-fn (make-restore-fn)] 29 | (is (zero? (count original-subs))) 30 | @(subscribe [:test-sub]) 31 | (is (one? (count @subs/query->reaction))) 32 | (is (contains? @subs/query->reaction [[:test-sub] []])) 33 | (restore-fn) 34 | (is (zero? (count @subs/query->reaction)))))) 35 | 36 | (deftest make-restore-fn-test2 37 | (testing "existing subs, making more subscriptions" 38 | (register-test-subs) 39 | @(subscribe [:test-sub]) 40 | (let [original-subs @subs/query->reaction 41 | restore-fn (make-restore-fn)] 42 | (is (one? (count original-subs))) 43 | @(subscribe [:test-sub2]) 44 | (is (contains? @subs/query->reaction [[:test-sub2] []])) 45 | (is (two? (count @subs/query->reaction))) 46 | (restore-fn) 47 | (is (not (contains? @subs/query->reaction [[:test-sub2] []]))) 48 | (is (one? (count @subs/query->reaction)))))) 49 | 50 | (deftest make-restore-fn-test3 51 | (testing "existing subs, making more subscriptions with different params on same subscriptions" 52 | (register-test-subs) 53 | @(subscribe [:test-sub]) 54 | (let [original-subs @subs/query->reaction 55 | restore-fn (make-restore-fn)] 56 | (is (one? (count original-subs))) 57 | @(subscribe [:test-sub :extra :params]) 58 | (is (two? (count @subs/query->reaction))) 59 | (restore-fn) 60 | (is (one? (count @subs/query->reaction)))))) 61 | 62 | (deftest nested-restores 63 | (testing "running nested restores" 64 | (register-test-subs) 65 | (let [restore-fn-1 (make-restore-fn) 66 | _ @(subscribe [:test-sub]) 67 | _ (is (one? (count @subs/query->reaction))) 68 | restore-fn-2 (make-restore-fn)] 69 | @(subscribe [:test-sub2]) 70 | (is (two? (count @subs/query->reaction))) 71 | (restore-fn-2) 72 | (is (one? (count @subs/query->reaction))) 73 | (restore-fn-1) 74 | (is (zero? (count @subs/query->reaction)))))) 75 | -------------------------------------------------------------------------------- /test/re-frame/rum_test.cljs: -------------------------------------------------------------------------------- 1 | (ns re-frame.rum-test 2 | (:require [cljs.test :refer-macros [is deftest use-fixtures]] 3 | [re-frame.core :as re-frame] 4 | [re-frame.db] 5 | [re-frame.registrar :as registrar] 6 | [re-frame.frank :as frank] 7 | [re-frame.rum :as re-rum] 8 | [rum.core :as rum] 9 | [cljsjs.react.dom.server])) 10 | 11 | ;; ---- FIXTURES --------------------------------------------------------------- 12 | 13 | ;; This fixture uses the re-frame.core/make-restore-fn to checkpoint and reset 14 | ;; to cleanup any dynamically registered handlers from our tests. 15 | (defn fixture-re-frame 16 | [] 17 | (let [restore-re-frame (atom nil)] 18 | {:before (fn [] 19 | (reset! restore-re-frame (re-frame.core/make-restore-fn)) 20 | (reset! re-frame.db/app-db {}) 21 | (registrar/clear-handlers)) 22 | :after #(@restore-re-frame)})) 23 | 24 | 25 | (use-fixtures :each (fixture-re-frame)) 26 | 27 | ;; ---- Rum components --------------------------------------------------------- 28 | 29 | (defn extract-counter [rum-state] 30 | (-> rum-state 31 | :frank/frank 32 | deref 33 | :counter)) 34 | 35 | (rum/defcs leaf-view 36 | < re-rum/with-frank 37 | [rum-state] 38 | [:h1 (str "Counter: " (extract-counter rum-state))]) 39 | 40 | (rum/defc intermediary-view [] 41 | [:div (leaf-view)]) 42 | 43 | (rum/defc root-view 44 | < (re-rum/inject-frank-into-context first) 45 | [frank] 46 | (intermediary-view)) 47 | 48 | ;; ---- TESTS ------------------------------------------------------------------ 49 | 50 | (deftest inject-and-access-react-context 51 | (re-frame/reg-event-db ::init 52 | (fn [db [_]] 53 | (assoc db :counter 0))) 54 | 55 | (let [frank (frank/create)] 56 | (frank/dispatch-sync! frank [::init]) 57 | 58 | (is (= "

Counter: 0

" 59 | (js/ReactDOMServer.renderToStaticMarkup (root-view frank)))))) 60 | -------------------------------------------------------------------------------- /test/re-frame/test_runner.cljs: -------------------------------------------------------------------------------- 1 | (ns re-frame.test-runner 2 | (:refer-clojure :exclude (set-print-fn!)) 3 | (:require 4 | [cljs.test :as cljs-test :include-macros true] 5 | [jx.reporter.karma :as karma :include-macros true] 6 | ;; Test Namespaces ------------------------------- 7 | [re-frame.interceptor-test] 8 | [re-frame.subs-test] 9 | [re-frame.fx-test] 10 | [re-frame.trace-test] 11 | [re-frame.restore-test] 12 | [re-frame.frank-test] 13 | [re-frame.rum-test] 14 | )) 15 | 16 | (enable-console-print!) 17 | 18 | ;; ---- BROWSER based tests ---------------------------------------------------- 19 | (defn ^:export set-print-fn! [f] 20 | (set! cljs.core.*print-fn* f)) 21 | 22 | 23 | (defn ^:export run-html-tests [] 24 | (cljs-test/run-tests 25 | 're-frame.interceptor-test 26 | 're-frame.subs-test 27 | 're-frame.fx-test 28 | 're-frame.trace-test 29 | 're-frame.restore-test 30 | 're-frame.frank-test 31 | 're-frame.rum-test 32 | )) 33 | 34 | ;; ---- KARMA ----------------------------------------------------------------- 35 | 36 | (defn ^:export run-karma [karma] 37 | (karma/run-tests 38 | karma 39 | 're-frame.interceptor-test 40 | 're-frame.subs-test 41 | 're-frame.fx-test 42 | 're-frame.trace-test 43 | 're-frame.restore-test 44 | 're-frame.frank-test 45 | 're-frame.rum-test 46 | )) 47 | -------------------------------------------------------------------------------- /test/re-frame/trace_test.cljs: -------------------------------------------------------------------------------- 1 | (ns re-frame.trace-test 2 | (:require [cljs.test :as test :refer-macros [is deftest]] 3 | [re-frame.trace :as trace :include-macros true] 4 | [re-frame.core :as rf])) 5 | 6 | (def test-traces (atom [])) 7 | 8 | (test/use-fixtures :once {:before (fn [] 9 | (trace/register-trace-cb :test 10 | (fn [traces] 11 | (doseq [trace traces] 12 | (swap! test-traces conj trace))))) 13 | :after (fn [] 14 | (trace/remove-trace-cb :test))}) 15 | 16 | (test/use-fixtures :each {:before (fn [] 17 | (reset! test-traces []) 18 | (trace/reset-tracing!))}) 19 | 20 | ; Disabled, as goog-define doesn't work in optimizations :whitespace 21 | ;(deftest trace-cb-test 22 | ; (trace/with-trace {:operation :test1 23 | ; :op-type :test}) 24 | ; (is (= 1 (count @test-traces))) 25 | ; (is (= (select-keys (first @test-traces) [:id :operation :op-type :tags]) 26 | ; {:id 1 :operation :test1 :op-type :test :tags nil}))) 27 | ; 28 | ;(enable-console-print!) 29 | ; 30 | ;(deftest sub-trace-test 31 | ; (rf/subscribe [:non-existence]) 32 | ; (is (= 1 (count @test-traces))) 33 | ; (is (= (select-keys (first @test-traces) [:id :operation :op-type :error]) 34 | ; {:id 1 :op-type :sub/create :operation :non-existence :error true}))) 35 | -------------------------------------------------------------------------------- /test/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | re-frame Unit Tests 4 | 5 | 6 | 7 | 8 | 9 | 36 | 37 | 38 | 39 |

re-frame Unit Tests

40 |
41 | 42 | 43 | 44 | 45 | 128 | 129 | 130 | --------------------------------------------------------------------------------