├── .editorconfig ├── .gitattributes ├── .github └── workflows │ ├── continuous-deployment-workflow.yml │ └── continuous-integration-workflow.yml ├── .gitignore ├── .idea └── codeStyleSettings.xml ├── CHANGELOG.md ├── CITATION.md ├── CONTRIBUTING.md ├── README.md ├── SUPPORT.md ├── bin ├── doctoc.bat └── doctoc.sh ├── book.json ├── deps.edn ├── docs ├── API.md ├── AnEpoch.md ├── App-Structure.md ├── ApplicationState.md ├── CodeWalkthrough.md ├── Coeffects.md ├── Debugging-Event-Handlers.md ├── Debugging.md ├── EPs │ ├── 001-CaptureHandlerMetadata.md │ ├── 002-ReframeInstances.md │ ├── 003-ReusableComponents.md │ ├── 004-ViewRegistration.md │ ├── 005-StateMachines.md │ └── README.md ├── EffectfulHandlers.md ├── Effects.md ├── EventHandlingInfographic.md ├── External-Resources.md ├── FAQs │ ├── CatchingEventExceptions.md │ ├── DB_Normalisation.md │ ├── DoINeedReFrame.md │ ├── FullStackReframe.md │ ├── GlobalInterceptors.md │ ├── Inspecting-app-db.md │ ├── LoadOnMount.md │ ├── Logging.md │ ├── Null-Dispatched-Events.md │ ├── PollADatabaseEvery60.md │ ├── README.md │ ├── UseASubscriptionInAnEventHandler.md │ ├── ViewsOnGlobalRegistration.md │ ├── When-Does-Dispatch-Happen.md │ ├── Why-CLJC.md │ └── Why-Clear-Sub-Cache.md ├── Figma Infographics │ └── inforgraphics.fig ├── INTRO.md ├── 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 ├── architecture_decision_log.org ├── images │ ├── Readme │ │ ├── 6dominoes.png │ │ ├── Dominoes-small.jpg │ │ ├── Dominoes.jpg │ │ └── todolist.png │ ├── epoch.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 │ ├── mental-model-omnibus.jpg │ ├── scale-changes-everything.jpg │ ├── subscriptions.png │ └── the-water-cycle.png └── styles │ └── website.css ├── examples ├── simple │ ├── .gitignore │ ├── README.md │ ├── project.clj │ ├── resources │ │ └── public │ │ │ ├── example.css │ │ │ └── example.html │ └── src │ │ ├── deps.cljs │ │ └── simple │ │ └── core.cljs └── todomvc │ ├── .gitignore │ ├── README.md │ ├── project.clj │ ├── resources │ └── public │ │ ├── index.html │ │ └── todos.css │ └── src │ ├── deps.cljs │ └── todomvc │ ├── core.cljs │ ├── db.cljs │ ├── events.cljs │ ├── subs.cljs │ └── views.cljs ├── images ├── Readme │ ├── 6dominoes.png │ ├── Dominoes-small.jpg │ ├── Dominoes.jpg │ └── todolist.png ├── epoch.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 ├── mental-model-omnibus.jpg ├── scale-changes-everything.jpg ├── subscriptions.png └── the-water-cycle.png ├── karma.conf.js ├── license.txt ├── project.clj ├── src ├── deps.cljs └── re_frame │ ├── cofx.cljc │ ├── context.clj │ ├── context.cljs │ ├── core.cljc │ ├── events.cljc │ ├── frame.cljc │ ├── fx.cljc │ ├── interceptor.cljc │ ├── interop.clj │ ├── interop.cljs │ ├── loggers.cljc │ ├── registry.cljc │ ├── router.cljc │ ├── std_interceptors.cljc │ ├── subs.cljc │ ├── trace.cljc │ └── utils.cljc └── test ├── re_frame ├── fx_test.cljs ├── interceptor_test.cljs ├── restore_test.cljs ├── router_test.clj ├── 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 | -------------------------------------------------------------------------------- /.github/workflows/continuous-deployment-workflow.yml: -------------------------------------------------------------------------------- 1 | name: cd 2 | on: 3 | push: 4 | tags: 5 | - "v[0-9]+.[0-9]+.[0-9]+*" 6 | 7 | jobs: 8 | test: 9 | name: Test 10 | runs-on: ubuntu-18.04 11 | container: 12 | # Ref: https://github.community/t5/GitHub-Actions/docker-pull-from-public-GitHub-Package-Registry-fail-with-quot/m-p/32782 13 | image: superstructor/clojure:openjdk-11-lein-2.9.1-node-browsers 14 | steps: 15 | - uses: actions/checkout@v1 16 | - name: Maven cache 17 | id: maven-cache 18 | uses: actions/cache@v1 19 | with: 20 | path: /root/.m2/repository 21 | key: ${{ runner.os }}-maven-${{ hashFiles('**/project.clj') }} 22 | restore-keys: | 23 | ${{ runner.os }}-maven- 24 | - name: npm cache 25 | uses: actions/cache@v1 26 | with: 27 | path: ~/.npm 28 | key: ${{ runner.os }}-npm-${{ hashFiles('project.clj') }}-${{ hashFiles('**/deps.cljs') }} 29 | restore-keys: | 30 | ${{ runner.os }}-npm- 31 | - name: shadow-cljs compiler cache 32 | uses: actions/cache@v1 33 | with: 34 | path: .shadow-cljs 35 | key: ${{ runner.os }}-shadow-cljs-${{ github.sha }} 36 | restore-keys: | 37 | ${{ runner.os }}-shadow-cljs- 38 | - if: steps.maven-cache.outputs.cache-hit != 'true' 39 | run: lein karma-once 40 | - if: steps.maven-cache.outputs.cache-hit != 'true' 41 | name: Run cd examples/simple && lein shadow compile client 42 | working-directory: examples/simple 43 | run: lein shadow compile client 44 | - if: steps.maven-cache.outputs.cache-hit != 'true' 45 | name: Run cd examples/todomvc && lein shadow compile client 46 | working-directory: examples/todomvc 47 | run: lein shadow compile client 48 | - if: steps.maven-cache.outputs.cache-hit == 'true' 49 | run: lein -o karma-once 50 | - if: steps.maven-cache.outputs.cache-hit == 'true' 51 | name: Run cd examples/simple && lein -o shadow compile client 52 | working-directory: examples/simple 53 | run: lein -o shadow compile client 54 | - if: steps.maven-cache.outputs.cache-hit == 'true' 55 | name: Run cd examples/todomvc && lein -o shadow compile client 56 | working-directory: examples/todomvc 57 | run: lein -o shadow compile client 58 | - name: Slack notification 59 | uses: homoluctus/slatify@v2.0.1 60 | if: failure() || cancelled() 61 | with: 62 | type: ${{ job.status }} 63 | job_name: re-frame Tests 64 | channel: '#oss-robots' 65 | url: ${{ secrets.SLACK_WEBHOOK }} 66 | commit: true 67 | token: ${{ secrets.GITHUB_TOKEN }} 68 | release: 69 | name: Release 70 | needs: test 71 | runs-on: ubuntu-18.04 72 | container: 73 | # Ref: https://github.community/t5/GitHub-Actions/docker-pull-from-public-GitHub-Package-Registry-fail-with-quot/m-p/32782 74 | image: superstructor/clojure:openjdk-11-lein-2.9.1-node-browsers 75 | steps: 76 | - uses: actions/checkout@v1 77 | - name: Maven cache 78 | id: maven-cache 79 | uses: actions/cache@v1 80 | with: 81 | path: /root/.m2/repository 82 | key: ${{ runner.os }}-maven-${{ hashFiles('**/project.clj') }} 83 | restore-keys: | 84 | ${{ runner.os }}-maven- 85 | - name: Run lein release 86 | if: steps.maven-cache.outputs.cache-hit != 'true' 87 | run: | 88 | CLOJARS_USERNAME=${{ secrets.CLOJARS_USERNAME }} CLOJARS_PASSWORD=${{ secrets.CLOJARS_PASSWORD }} GITHUB_USERNAME=${{ github.actor }} GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} lein release 89 | - name: Run lein -o release 90 | if: steps.maven-cache.outputs.cache-hit == 'true' 91 | run: | 92 | CLOJARS_USERNAME=${{ secrets.CLOJARS_USERNAME }} CLOJARS_PASSWORD=${{ secrets.CLOJARS_PASSWORD }} GITHUB_USERNAME=${{ github.actor }} GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} lein -o release 93 | - name: Slack notification 94 | uses: homoluctus/slatify@v2.0.1 95 | if: always() 96 | with: 97 | type: ${{ job.status }} 98 | job_name: re-frame Deployment 99 | channel: '#oss-robots' 100 | url: ${{ secrets.SLACK_WEBHOOK }} 101 | commit: true 102 | token: ${{ secrets.GITHUB_TOKEN }} 103 | -------------------------------------------------------------------------------- /.github/workflows/continuous-integration-workflow.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | push: 4 | paths-ignore: 5 | - "bin/**" 6 | - "docs/**" 7 | - "images/**" 8 | - ".editorconfig" 9 | - ".gitignore" 10 | - "CHANGELOG.md" 11 | - "CITATION.md" 12 | - "CONTRIBUTING.md" 13 | - "license.txt" 14 | - "README.md" 15 | 16 | jobs: 17 | test: 18 | name: Test 19 | runs-on: ubuntu-18.04 20 | container: 21 | # Ref: https://github.community/t5/GitHub-Actions/docker-pull-from-public-GitHub-Package-Registry-fail-with-quot/m-p/32782 22 | image: superstructor/clojure:openjdk-11-lein-2.9.1-node-browsers 23 | steps: 24 | - uses: actions/checkout@v1 25 | - name: Maven cache 26 | id: maven-cache 27 | uses: actions/cache@v1 28 | with: 29 | path: /root/.m2/repository 30 | key: ${{ runner.os }}-maven-${{ hashFiles('**/project.clj') }} 31 | restore-keys: | 32 | ${{ runner.os }}-maven- 33 | - name: npm cache 34 | uses: actions/cache@v1 35 | with: 36 | path: ~/.npm 37 | key: ${{ runner.os }}-npm-${{ hashFiles('project.clj') }}-${{ hashFiles('**/deps.cljs') }} 38 | restore-keys: | 39 | ${{ runner.os }}-npm- 40 | - name: shadow-cljs compiler cache 41 | uses: actions/cache@v1 42 | with: 43 | path: .shadow-cljs 44 | key: ${{ runner.os }}-shadow-cljs-${{ github.sha }} 45 | restore-keys: | 46 | ${{ runner.os }}-shadow-cljs- 47 | - if: steps.maven-cache.outputs.cache-hit != 'true' 48 | run: lein karma-once 49 | - if: steps.maven-cache.outputs.cache-hit != 'true' 50 | name: Run cd examples/simple && lein shadow compile client 51 | working-directory: examples/simple 52 | run: lein shadow compile client 53 | - if: steps.maven-cache.outputs.cache-hit != 'true' 54 | name: Run cd examples/todomvc && lein shadow compile client 55 | working-directory: examples/todomvc 56 | run: lein shadow compile client 57 | - if: steps.maven-cache.outputs.cache-hit == 'true' 58 | run: lein -o karma-once 59 | - if: steps.maven-cache.outputs.cache-hit == 'true' 60 | name: Run cd examples/simple && lein -o shadow compile client 61 | working-directory: examples/simple 62 | run: lein -o shadow compile client 63 | - if: steps.maven-cache.outputs.cache-hit == 'true' 64 | name: Run cd examples/todomvc && lein -o shadow compile client 65 | working-directory: examples/todomvc 66 | run: lein -o shadow compile client 67 | - name: Slack notification 68 | uses: homoluctus/slatify@v2.0.1 69 | if: failure() || cancelled() 70 | with: 71 | type: ${{ job.status }} 72 | job_name: re-frame Tests 73 | channel: '#oss-robots' 74 | url: ${{ secrets.SLACK_WEBHOOK }} 75 | commit: true 76 | token: ${{ secrets.GITHUB_TOKEN }} 77 | -------------------------------------------------------------------------------- /.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 | docs/build/_book/ 25 | docs/build/index.html 26 | docs/**/*.epub 27 | docs/**/*.pdf 28 | docs/**/*.mobi 29 | build/ 30 | *~ 31 | /package.json 32 | /package-lock.json 33 | /shadow-cljs.edn 34 | /.shadow-cljs/ 35 | .cpcache 36 | -------------------------------------------------------------------------------- /.idea/codeStyleSettings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 21 | -------------------------------------------------------------------------------- /CITATION.md: -------------------------------------------------------------------------------- 1 | To cite re-frame in publications, please use: 2 | 3 | [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.801613.svg)](https://doi.org/10.5281/zenodo.801613) 4 | 5 | Thompson, M. (2015, March). Re-Frame: A Reagent Framework For Writing SPAs, in Clojurescript. 6 | Zenodo. http://doi.org/10.5281/zenodo.801613 7 | 8 | @misc{thompson_2015, 9 | author = {Thompson, Michael}, 10 | title = {Re-Frame: A Reagent Framework For Writing SPAs, in Clojurescript.}, 11 | month = mar, 12 | year = 2015, 13 | doi = {10.5281/zenodo.801613}, 14 | url = {https://doi.org/10.5281/zenodo.801613} 15 | } 16 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to re-frame 2 | 3 | Thank you for taking the time to contribute! 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 master branch.** 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 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | ## Support questions 2 | 3 | The Github issues are for bug reports and feature requests only. Support requests and usage 4 | questions should go to the re-frame [Clojure Slack channel](http://clojurians.net) or 5 | the [ClojureScript mailing list](https://groups.google.com/forum/#!forum/clojurescript). 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /book.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "re-frame", 3 | "description": "Documentation of re-frame framework", 4 | "language": "en", 5 | "root": "./docs/", 6 | "styles": { 7 | "website": "styles/website.css" 8 | }, 9 | "structure": { 10 | "readme": "INTRO.md", 11 | "summary": "README.md" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | 2 | ;; Allows users of deps.edn to more conveniently fork / make PRs to re-frame 3 | 4 | {:deps {net.cgrand/macrovich {:mvn/version "0.2.1"} 5 | org.clojure/tools.logging {:mvn/version "0.4.1"} 6 | com.lambdaisland/glogi {:mvn/version "1.0.116"} 7 | reagent/reagent {:mvn/version "0.9.1" 8 | :exclusions [cljsjs/react cljsjs/react-dom cljsjs/react-dom-server]}} 9 | :aliases 10 | {:dev 11 | {:extra-deps {org.clojure/clojure {:mvn/version "1.10.1"} 12 | org.clojure/clojurescript {:mvn/version "1.10.844"} 13 | }}}} 14 | -------------------------------------------------------------------------------- /docs/API.md: -------------------------------------------------------------------------------- 1 | ## The re-frame API 2 | 3 | Orientation: 4 | 1. The API is provided by `re-frame.core`: 5 | - at some point, it would be worth your time [to browse it](/src/re_frame/core.cljc) 6 | - to use re-frame, you'll need to `require` it, perhaps like this ... 7 | ```clj 8 | (ns my.namespace 9 | (:require [re-frame.core :as rf])) 10 | 11 | ... now use rf/reg-event-fx or rf/subscribe 12 | ``` 13 | 2. The API is small. Writing an app, you use less than 10 API functions. Maybe just 5. 14 | 3. There's no auto-generated docs [because of this problem](/src/re_frame/core.cljc#L23-L36) 15 | but, as a substitute, 16 | the links below take you to the doc-strings of often-used API functions. 17 | 18 | ## Doc Strings For The Significant API Functions 19 | 20 | The core API consists of: 21 | - [dispatch](/src/re_frame/router.cljc#L233-L243) or [dispatch-sync](/src/re_frame/router.cljc#L251-L263). 22 | - [reg-event-db](/src/re_frame/core.cljc#L71-L80) or [reg-event-fx](/src/re_frame/core.cljc#L87-L97) 23 | - [reg-sub](/src/re_frame/subs.cljc#L200-L329) and [subscribe](/src/re_frame/subs.cljc#L74-L115) working together 24 | 25 | Occasionally, you'll need to use: 26 | - [reg-fx](/src/re_frame/fx.cljc#L18-L41) 27 | - [reg-cofx](/src/re_frame/cofx.cljc#L14-L22) and [inject-cofx](/src/re_frame/cofx.cljc#L29-L80) working together 28 | 29 | And, finally, there are the builtin Interceptors: 30 | - [path](/src/re_frame/std_interceptors.cljc#L182-L208) 31 | - [after](/src/re_frame/std_interceptors.cljc#L295-L305) 32 | - [debug](/src/re_frame/std_interceptors.cljc#L14-L40) 33 | - and browse [the others](/src/re_frame/std_interceptors.cljc) 34 | 35 | 36 | ## Built-in Effect Handlers 37 | 38 | The following built-in effects are also a part of the API: 39 | 40 | #### :dispatch-later 41 | 42 | `dispatch` one or more events after given delays. Expects a collection 43 | of maps with two keys: `:ms` and `:dispatch` 44 | 45 | usage: 46 | ```clj 47 | {:dispatch-later [{:ms 200 :dispatch [:event-id "param"]} 48 | {:ms 100 :dispatch [:also :this :in :100ms]}]} 49 | ``` 50 | 51 | Which means: in 200ms do this: `(dispatch [:event-id "param"])` and in 100ms ... 52 | 53 | Note: nil entries in the collection are ignored which means events can be added 54 | conditionally: 55 | 56 | ```clj 57 | {:dispatch-later [ (when (> 3 5) {:ms 200 :dispatch [:conditioned-out]}) 58 | {:ms 100 :dispatch [:another-one]}]} 59 | ``` 60 | 61 | *** 62 | 63 | #### :dispatch 64 | 65 | `dispatch` one event. Expects a single vector. 66 | 67 | usage: 68 | ```clj 69 | {:dispatch [:event-id "param"] } 70 | ``` 71 | 72 | *** 73 | 74 | #### :dispatch-n 75 | 76 | `dispatch` more than one event. Expects a collection events. 77 | 78 | usage: 79 | ```clj 80 | {:dispatch-n (list [:do :all] [:three :of] [:these])} 81 | ``` 82 | Note 1: The events supplied will be dispatched in the order provided. 83 | Note 2: nil events are ignored which means events can be added 84 | conditionally: 85 | ```clj 86 | {:dispatch-n (list (when (> 3 5) [:conditioned-out]) 87 | [:another-one])} 88 | ``` 89 | 90 | *** 91 | #### :deregister-event-handler 92 | 93 | removes a previously registered event handler. Expects either a single id 94 | (typically a keyword), or a seq of ids. 95 | 96 | usage: 97 | ```clj 98 | {:deregister-event-handler :my-id)} 99 | ``` 100 | or: 101 | ```clj 102 | {:deregister-event-handler [:one-id :another-id]} 103 | ``` 104 | *** 105 | #### :db 106 | 107 | reset! app-db with a new value. Expects a map. 108 | 109 | usage: 110 | ```clj 111 | {:db {:key1 value1 :key2 value2}} 112 | ``` 113 | 114 | *** 115 | 116 | Previous: [Infographic: A re-frame Epoch](AnEpoch.md)       117 | Up: [Index](README.md)       118 | Next: [Infographic: Event Processing](EventHandlingInfographic.md)       119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /docs/AnEpoch.md: -------------------------------------------------------------------------------- 1 | ## One re-frame Epoch 2 | 3 | The following graphic shows how, operationally, the six dominoes play out, over time, within the browser. 4 | 5 | 6 | 7 | *** 8 | 9 | Previous: [First Code Walk-Through](CodeWalkthrough.md)       10 | Up: [Index](README.md)       11 | Next: [The API](API.md)       12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /docs/App-Structure.md: -------------------------------------------------------------------------------- 1 | ## Simpler Apps 2 | 3 | To build a re-frame app, you: 4 | - design your app's data structures (data layer) 5 | - write Reagent view functions (domino 5) 6 | - write event handler functions (control layer and/or state transition layer, domino 2) 7 | - write subscription functions (query layer, domino 4) 8 | 9 | For simpler apps, you should put code for each layer into separate files: 10 | ``` 11 | src 12 | ├── core.cljs <--- entry point, plus history, routing, etc 13 | ├── db.cljs <--- schema, validation, etc (data layer) 14 | ├── views.cljs <--- reagent views (view layer) 15 | ├── events.cljs <--- event handlers (control/update layer) 16 | └── subs.cljs <--- subscription handlers (query layer) 17 | ``` 18 | 19 | For a living example of this approach, look at the [todomvc example](https://github.com/day8/re-frame/tree/master/examples/todomvc). 20 | 21 | *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.* 22 | 23 | ### There's A Small Gotcha 24 | 25 | If you adopt this structure, there's a gotcha. 26 | 27 | `events.cljs` and `subs.cljs` will never be `required` by any other 28 | namespaces. To the Google Closure dependency mechanism, it appears as 29 | if these two namespaces are not needed and it doesn't load them. 30 | 31 | And, if the namespaces are not loaded, the registrations in these namespaces will 32 | never happen. And, then you'll be staring at your running app very 33 | puzzled about why none of your events handlers are registered. 34 | 35 | Once you twig to what's going on, the solution is easy. You must 36 | explicitly `require` both namespaces, `events` and `subs`, in your `core` 37 | namespace. Then they'll be loaded and the registrations (`reg-sub`, `reg-event-fx`, 38 | etc) will occur as that loading happens. 39 | 40 | ## Larger Apps 41 | 42 | Assuming your larger apps have multiple "panels" (or "views") which are 43 | relatively independent, you might use this structure: 44 | ``` 45 | src 46 | ├── core.cljs <--- entry point, plus history, routing, etc 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 | If you follow this structure you should probably use namespaced keywords instead of simple keywords. 63 | 64 | This gives the ability to encapsulate the state of each "panel" and ensure you don't get any conflicts. 65 | 66 | 67 | Suppose for example that in your panel you want to store a value `x` in the db, if you want to use 68 | namespaced keywords you the event handler and subscription will look like this: 69 | 70 | ```clj 71 | (rf/reg-event-db 72 | ::set-x 73 | (fn [db [_ value]] 74 | (assoc db ::x value))) 75 | 76 | (rf/reg-sub 77 | ::x 78 | (fn [db _] 79 | (get db ::x))) 80 | ``` 81 | 82 | If you want to dispatch that even you have two options, either: 83 | 84 | ```clj 85 | (require [project.panel.handlers :as handlers]) 86 | 87 | (rf/dispatch [::handlers/set-x 100]) 88 | ``` 89 | 90 | or: 91 | 92 | ```clj 93 | (rf/dispatch [:project.panel.handlers/set-x 100]) 94 | ``` 95 | 96 | Where the first option might be preferrable since it ensures you require the handlers file and saves you from the possibility of typos. 97 | 98 | ## I Want Real Examples! 99 | 100 | Maybe look here: 101 | https://github.com/day8/re-frame/blob/master/docs/External-Resources.md#examples-and-applications-using-re-frame 102 | 103 | *** 104 | 105 | Previous: [Correcting a wrong](SubscriptionsCleanup.md)       106 | Up: [Index](README.md)       107 | Next: [Navigation](Navigation.md) 108 | 109 | 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /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/EPs/001-CaptureHandlerMetadata.md: -------------------------------------------------------------------------------- 1 | ## EP 001 - Better Capture Of Handler Metadata 2 | 3 | > Status: Drafting. May be incoherent and/or wrong. Don't read. 4 | 5 | ### Abstract 6 | 7 | This EP proposes changes to the way re-frame handlers are registered, 8 | to allow for the capture of richer handler metadata. 9 | These changes also lay the groundwork for tooling advances, and EPs to follow. 10 | 11 | 12 | ### Background 13 | 14 | re-frame's API currently includes 7 functions for registering handlers: 15 | - event: `reg-event-db`, `reg-event-fx` and `reg-event-ctx` 16 | - subscription: `reg-sub` and `reg-sub-raw` 17 | - effects: `reg-fx` 18 | - coeffects: `reg-cofx` 19 | 20 | Two others are on the drawing board: 21 | - `reg-view` 22 | - `reg-interceptor` 23 | 24 | So, there are potentially 9 `kinds` of handlers. 25 | 26 | Internally, re-frame manages registered handlers in a `registrar`, which is a two-level map, 27 | keyed at the first level by the `kind` of handler and at the second level by the (keyword) 28 | `id` of the handler. The leaf values are the handler functions themselves. 29 | 30 | 31 | ## Introduction 32 | 33 | This EP proposes that: 34 | 1. all current registration functions in the API be superseded by a new single macro `reg` 35 | 2. the leaf nodes of the `registrar`, which are currently the handler functions themselves, 36 | become instead a map of values related to the handler, 37 | including a doc string, the file/line where defined, specs, etc, and, of course, 38 | the handler itself. 39 | 40 | 41 | ## Motivations 42 | 43 | There's pressure from multiple directions to collect and retain more metadata about handlers: 44 | - tickets like [#457](https://github.com/day8/re-frame/issues/457) want docstrings for handlers 45 | - adding specs for events, so they can be checked at dev time 46 | - when re-frame becomes less of a framework and more of a library, handlers might 47 | need be "grouped" into "packages". So "package" information about handlers need to be supplied and retained. 48 | - Tooling support - we'd like `re-frame-10x` to actively help programmers when they are learning a 49 | new code base. That's one of [the four stated goals](https://github.com/day8/re-frame-10x#helps-me-how). 50 | Ideally, re-frame would be capable of providing tooling with "a complete 51 | inventory" of an app's handlers, along with useful 52 | metadata on each handles. When an event is processed, the audit trail of 53 | handlers involved should be rich with information. 54 | 55 | ## Macro 56 | 57 | As part of the retained handler metadata, we'd like to automatically capture 58 | source code coordinates, like namespace and line number. 59 | To make this happen, a macro will need to be introduced for registration, and that's a shift in 60 | approach because, until now, macros have been manfully resisted. 61 | 62 | Introducing docstrings into registrations also encourages 63 | a macro solution because docstrings should be removed from 64 | production builds. 65 | 66 | ## Method 67 | 68 | A new macro `reg` will become the method 69 | of registering handlers. The existing 7 registration functions 70 | will ultimately be deprecated. 71 | 72 | `reg` will take one argument, a map, which captures all aspects of 73 | the handler. 74 | 75 | ## Examples 76 | 77 | Previously, `reg-event-db` was used like this: 78 | ```clj 79 | (rf/reg-event-db 80 | :event-id 81 | (fn [db event] 82 | (assoc db :some :thing))) 83 | ``` 84 | 85 | now, use `reg` would be used like this: 86 | ```clj 87 | (rf/reg 88 | {:kind :event-db 89 | :id :event-id 90 | :fn (fn [db event] 91 | (assoc db :some :thing))}) 92 | ``` 93 | 94 | The map argument must contain the keys `:kind`, `:id` and `:fn`, 95 | with other valid keys being dependent on the `kind` of 96 | handler being registered. 97 | 98 | The value `:kind` can be one these 7 (mapping to 7 existing `reg-*` functions): 99 | - `:event-db` `:event-fx` `:event-ctx` 100 | - `:sub` `:sub-raw` 101 | - `:fx` 102 | - `:cofx` 103 | 104 | Optionally, for all `kinds` of handlers the 105 | the map can also have these additional keys: 106 | - `:doc` a doc string 107 | - `:ns` the namespace where the handler was registered 108 | - `:line` line number where the handler was registered 109 | - `:file` the name file where the handler was registered 110 | 111 | In a dev build, the `reg` macro will supply the final 3 (source code coordinates), 112 | if not explicitly supplied in the map. 113 | 114 | In a production build, the `:doc` string will be elided, so we do not 115 | appear in the final source code at all. 116 | 117 | The key `:pkg` is reserved for future use, and might eventually indicate the 118 | "package" to which this handler belongs. See EP 002. 119 | 120 | Other keys: XXX 121 | - `:cept` for interceptors (when registering events) 122 | - `:ins` for input signals (when registering subscriptions) 123 | - `:ret` for return spec (subscriptions and events) 124 | - `:spec` for event spec (when registering events) ??? Too much ?? 125 | 126 | XXX I'm not entirely happy about using short names like `:cept`. But, then 127 | again, there's the aesthetics of formatting the code and lining things up. 128 | 129 | XXX could have a `:cofx` key for event handlers to make them more explicit. 130 | 131 | ### Multiple Registrations 132 | 133 | The argument to `reg` can also be a vector of maps to allow 134 | for multiple handlers to be registered at once: 135 | 136 | ```clj 137 | (rf/reg 138 | [{:kind :event-db ...} 139 | {:kind :event-db ...} 140 | {:kind :event-fx ...} 141 | {:kind :sub ...]) 142 | ``` 143 | 144 | XXX maybe not needed. Provide the most minimal API? Then let towers of abstraction be built on top. 145 | 146 | ### Registrar 147 | 148 | Each entry stored in the registrar will become a map instead of just a handler. 149 | 150 | Map keys: 151 | - `:kind` - somehow redundant 152 | - `:id` 153 | - `:doc` 154 | - `:line` 155 | - `:ns` 156 | - `:doc` 157 | - `:fn` the handler 158 | 159 | XXX look into reg-sub 160 | 161 | ### Backwards Compatibility 162 | 163 | XXX 164 | 165 | ## Issues/Questions/Todos 166 | 167 | - XXX implications for Cursive - it currently special-cases re-frame registration function -- give ColinF a heads up?? 168 | - XXX Dear God, consider changes to documentation/tutorials 169 | - XXX means giving up syntax sugar for reg-sub ? 170 | - XXX any format for `:doc` for display in HTML? Or just texual. 171 | 172 | 173 | 174 | -------------------------------------------------------------------------------- /docs/EPs/003-ReusableComponents.md: -------------------------------------------------------------------------------- 1 | ## EP 003 - Enabling Creation Of Reusable Components 2 | 3 | > Status: Placeholder. Don't bother reading yet. 4 | 5 | ### Abstract 6 | 7 | This EP proposes changes to facilitate easier use of React's Context feature. 8 | XXX to make it easier to write more complex Reusable components. 9 | 10 | ### Background 11 | 12 | React components form a tree, with values being passed down 13 | through the hierarchy in the form of `props`. All very functional. 14 | 15 | Except there are some problems: 16 | - it can be a PITA to pass every little bit of data through many, many layers. 17 | Manual and time consuming. 18 | - often we don't want to burden intermediate layers with knowledge about 19 | what leaf nodes needed. That kind of "unnecessary knowing" leads to 20 | various kinds of fragility. 21 | - if we are using someone else's layout components, we may have not have 22 | control over what they pass to children. 23 | 24 | [Algebraic Effects](http://math.andrej.com/eff/) are intended to help solve 25 | these kinds of problems in a functional way, but that's not our world. 26 | 27 | The solution available in React is called `Context`. It is a mechanism for allowing 28 | data to be "shared" globally within a given tree of React components 29 | (without it needing for it to be passed/threaded through all layers of that tree). 30 | 31 | [React's context docs are here](https://reactjs.org/docs/context.html). 32 | 33 | ### Components 34 | 35 | `re-com` is a library which supplies reusable Reagent widgets. And `widgets`, 36 | like a datepicker, are the simplest kind of components. 37 | 38 | `re-com` components are reusability because they take `an input stream` of data 39 | and they 40 | 41 | achieves reusablity by passing in values and suppling callbacks. This works at 42 | the level of simple widgets. 43 | 44 | But re-frame components need to subscribe and dispatch. 45 | 46 | XXX talk about `dispatch back` rather than `callback` 47 | 48 | XXX need to identify the part of `app-db` on which `event handlers` and `subscriptions` should operate. 49 | 50 | 51 | -------------------------------------------------------------------------------- /docs/EPs/004-ViewRegistration.md: -------------------------------------------------------------------------------- 1 | ## EP 003 - View Registration 2 | 3 | > Status: Placeholder. Only scribbles. Don't read yet. 4 | 5 | 6 | ### Abstract 7 | 8 | Broadbrush: 9 | - views will be registered via a new `re-frame.core/def-view` function 10 | - like other re-frame registration functions, `def-view` associates a `keyword` with a (reagent) render function 11 | - the registered view keyword (eg: `:my-view`) can be used in hiccup to identify the renderer. eg: `[:my-view "Hello world"]` 12 | - `def-view` allows various values to be `injected` as args into the view render 13 | - see https://github.com/reagent-project/reagent/issues/362 14 | 15 | Why: 16 | - removing (render) functions from hiccup will make hiccup even more data oriented. Symptoms include helping with various state machine ideas. 17 | - injection of `dispatch` and `subscribe` will help view functions to be slightly more pure. `dispatch` still kinda a problem. 18 | - ijection of `context` which will help with "multiple re-frame apps on the one page" problem 19 | 20 | What might need to be injected (as args) into a view: 21 | 22 | - `subscribe` and `dispatch` 23 | - a `frame` supplied via `context` (subscribe and dispatch obtained from frame) 24 | - other context: data from higher in the DOM tree 25 | - annimation? CSS ? 26 | 27 | XXX searches up the DOM heirarchy looking for a `frame` context then extracts dispatch and subscribe. Sounds inefficient. 28 | 29 | ### Code Doodle #1 30 | 31 | Associate the keyword `:my-view-id ` with a renderer using `def-view`: 32 | ```clj 33 | (def-view 34 | :my-view-id 35 | 36 | ;; indicate what `context` is required 37 | [:dispatch :subscribe :context XXX] 38 | 39 | ;; the renderer 40 | ;; last argument `context` is a map of: 41 | ;; - `:subs` - a vector of subscription values? 42 | ;; - :dispatch and :subscribe 43 | ;; - :context - a vector of context values 44 | ;; 45 | (fn [a-str context] 46 | (let [XXXX] 47 | ))) 48 | ``` 49 | 50 | Use of `:my-view-id `: 51 | ```clj 52 | [:my-view-id "Hello"] 53 | ``` 54 | 55 | ### Code Doodle #2 56 | 57 | Associate the keyword `:my-view-id ` with a renderer using `def-view`: 58 | ```clj 59 | (def-view 60 | :my-view-id 61 | 62 | ;; injection function 63 | ;; indicate what subscriptions we wish to obtain 64 | ;; obtain a dispatch for use 65 | ;; get the context id if you want to 66 | ;; 67 | :subscriptions 68 | (fn [_ id] 69 | {:subs [[:item ]] 70 | :context ["name1", "name2")}) 71 | 72 | 73 | ;; the renderer 74 | ;; last argument `ins` is a map of: 75 | ;; - `:subs` - a vector of subscription values? 76 | ;; - :dispatch and :subscribe 77 | ;; - :context - a vector of context values 78 | ;; 79 | (fn [a-str ins] 80 | (let [XXXX] 81 | ))) 82 | ``` 83 | 84 | Use of `:my-view-id `: 85 | ```clj 86 | [:my-view-id "Hello"] 87 | ``` 88 | 89 | ### Code Doodle #3 90 | 91 | `[:something arg1 arg2]` 92 | 93 | ```clj 94 | (def-view 95 | :something 96 | (fn [arg1 arg2] 97 | ;; obtain dispatch and subscription 98 | ;; obtain a subscription ot two 99 | ;; add a key on the component 100 | (fn [arg1 arg2] 101 | )) 102 | 103 | ``` 104 | 105 | ## Misc Notes 106 | 107 | - reagent hiccup will be changed/monkey-patched so that views can be identified by keyword 108 | - Views are the leaves of the signal graph. They need to subscribe and dispatch. 109 | - how to obtain other pieces of `context` (beyond the current frame) 110 | 111 | 112 | XXX There's a nasty problem with frames and subscriptions. How does the signal function know what frame to create new subscriptions against??? 113 | 114 | ## Usage 115 | 116 | 117 | -------------------------------------------------------------------------------- /docs/EPs/005-StateMachines.md: -------------------------------------------------------------------------------- 1 | ## EP 003 - Finite State Machines 2 | 3 | > Status: Placeholder. Don't bother reading yet. 4 | 5 | ### Abstract 6 | 7 | Colloquial: I'd like re-frame to have a better story for programming in FSMs. I want to 8 | represent "Higher Order Behaviour" which currently gets "smeared" across 9 | multiple event handlers. 10 | 11 | ### Introduction 12 | 13 | Many **high level** aspects of SPAs are best modelled explicitly as an FSM. 14 | First, an app must gather X from the user, but if they press cancel, then return to 15 | showing Y, but otherwise move on to do activity Z. It is quite natural to model such overall 16 | decisions and the UIs involved as an FSM. 17 | 18 | Many **low level** aspects of SPAs are best modelled explicitly as FSM too. 19 | The simple act of GETing from a server involves various 20 | states, including waiting, and failed, and timed-out, and retrying and succeeded. 21 | 22 | BUT you need the power of a fully expressive state machine. 23 | You need orthogonal state, guards, hierarchical state, history, etc. 24 | Back in 1987, Harel identified the set of features required - anything less 25 | and your tool will not be expressive enough to do the job. 26 | Harel also insisted that statecharts was a visual 27 | formalism, so tooling is also important. 28 | 29 | ### Why? 30 | 31 | > Beware of the Turing tar-pit in which everything is possible but nothing of interest is easy.
32 | > -- Alan Perlis 33 | 34 | State machines are appealing because: 35 | - they constrain you (vs the full Turing tar-pit). Just because you *can* wield 36 | immense power doesn't mean you should. 37 | - they force you to think through and structure a problem. This process helps to flush out the corner cases. 38 | - they make explicit certain important assumptions which are otherwise hidden in a thicket of conditionals. 39 | 40 | 41 | Also, Why Developers Never Use State Machines 42 | https://www.skorks.com/2011/09/why-developers-never-use-state-machines/ 43 | 44 | > "The strength of JavaScript is that you can do anything. The weakness is that you will."
45 | > - Reg Braithwaite 46 | 47 | ### How? 48 | 49 | Technical elements: 50 | - a way to register: 51 | - a state machine specification against a `machine-id` 52 | - the specification will be a data structure, listing states, transitions, etc 53 | - a way to create an instance of a registered state machine 54 | - args: machine-id, id for this particular instance 55 | - data for machine instance will be stored in `app-db` (at `:machines` or a configurable place?) 56 | - a way to trigger 57 | - the `id` of the state machine targeted 58 | - the trigger 59 | - the trigger args 60 | - trigger causes: 61 | - state transition 62 | - an action fn to be called which produces `:effects` 63 | - UI changes. See EP on `reg-view` which will make it much easier to describe UI in machine data structure 64 | - a way to destroy an instance 65 | 66 | ### Misc Notes 67 | 68 | Events model user intent, not implementation details. 69 | 70 | So, we DO NOT want events that talk about FSM or triggers etc because that's an implementation detail. 71 | 72 | Instead, we want 73 | 74 | 75 | But event handlers should know about XXX 76 | 77 | 78 | ### Triggers 79 | 80 | 81 | Types of triggers: 82 | 1. External (from the user, websocket) 83 | 2. Data - something about `app-db` has changed 84 | 85 | ### Implementation 86 | 87 | What if we didn't even use FSM and used Behaviour Trees instead? 88 | Behaviour trees are more composable. A better match for a data-oriented design. 89 | 90 | 91 | Links And Notes: 92 | 93 | - [statecharts](https://statecharts.github.io/) 94 | - [THE PRECISE SEMANTICS OF UML STATE MACHINES](https://www.omg.org/spec/PSSM/1.0/Beta1/PDF) 95 | - [David Khourshid - Infinitely Better UIs with Finite Automata](https://www.youtube.com/watch?v=VU1NKX6Qkxc) also [written](https://css-tricks.com/robust-react-user-interfaces-with-finite-state-machines/) 96 | - [Statecharts: Updating UI state](https://medium.com/@lmatteis/statecharts-updating-ui-state-767052b6b129) 97 | - [https://statecharts.github.io/](https://statecharts.github.io/) 98 | - [Harel Paper](http://www.inf.ed.ac.uk/teaching/courses/seoc/2005_2006/resources/statecharts.pdf) 99 | - [setState Machine Speaker Deck by Michele Bertoli](https://speakerdeck.com/michelebertoli/setstate-machine) 100 | 101 | 102 | 103 | Previously CLJS : 104 | - https://github.com/jebberjeb/reframe-fsm 105 | - https://github.com/protocol55/re-state 106 | - http://blog.cognitect.com/blog/2017/8/14/restate-your-ui-creating-a-user-interface-with-re-frame-and-state-machines 107 | 108 | 109 | Other Attempts: 110 | - [Fractal UI components using snabbdom, Harel statecharts, and event emitters](https://github.com/jayrbolton/snabbdom-statechart-components) 111 | 112 | - [BT 101 – Behavior Trees grammar basics](http://www.craft.ai/blog/bt-101-behavior-trees-grammar-basics/) 113 | - [Understanding Behavior Trees](http://aigamedev.com/open/article/bt-overview/) 114 | - [behavior3js github repo](https://github.com/behavior3/behavior3js) 115 | - [Understanding the Second Generation of Behavior Trees (video)](https://www.youtube.com/watch?v=n4aREFb3SsU) 116 | - [10 Reasons the Age of Finite State Machines is Over](http://aigamedev.com/open/article/fsm-age-is-over/) 117 | - [Parameterizing Behavior Trees](https://people.cs.umass.edu/~fmgarcia/Papers/Parameterizing%20Behavior%20Trees.pdf) 118 | - [Behavior Trees in Robotics and AI](https://arxiv.org/pdf/1709.00084.pdf) 119 | 120 | TLA+ 121 | - [** Three Approximations - includes SAM](https://dzone.com/articles/the-three-approximations-you-should-never-use-when) 122 | - [State Machines and Computing](https://www.ebpml.org/blog2/index.php/2015/01/16/state-machines-and-computing) 123 | - [SAM – the State-Action-Model pattern](https://www.ebpml.org/blog15/2015/06/sam-the-state-action-model-pattern/) 124 | - [TLA Intro](https://lamport.azurewebsites.net/tla/tla-intro.html) 125 | - [Computation and State Machines - Leslie Lamport](https://www.microsoft.com/en-us/research/publication/computation-state-machines/) 126 | - [SAM Pattern in JS](http://sam.js.org/) 127 | - 128 | 129 | 130 | -------------------------------------------------------------------------------- /docs/EPs/README.md: -------------------------------------------------------------------------------- 1 | EP 2 | 3 | This directory contains re-frame "Enhancement Proposals" (EPs), one per file. 4 | 5 | ## Status 6 | 7 | Each EP proceeds through the following stages: 8 | - **Placeholder** - skeleton at best. One day someone might write something. 9 | - **Drafting** - some writing and thinking has been done, but it may be incoherent and/or wrong. 10 | - **UnderReview** - ready for general discussion in a specifically created (repo) Issue. 11 | - **Accepted** or **Rejected** 12 | - **Released** 13 | 14 | 15 | ## List 16 | 17 | - [EP 001 - Capture Handler Metadata](001-CaptureHandlerMetadata.md) - Drafting 18 | - [EP 002 - Multiple re-frame Instances](002-ReframeInstances.md) - Drafting 19 | - [EP 003 - Reusable Components Via context](003-ReusableComponents.md) - Placeholder 20 | - [EP 004 - View Registration](004-ViewRegistration.md) - Placeholder 21 | - [EP 005 - State Machines](005-StateMachines.md) - Placeholder 22 | 23 | -------------------------------------------------------------------------------- /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: [The API](API.md)       16 | Up: [Index](README.md)       17 | Next: [Effectful Handlers](EffectfulHandlers.md)       18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /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/DB_Normalisation.md: -------------------------------------------------------------------------------- 1 | ### Question 2 | 3 | `app-db` contains a `map`. How do I store normalised data in a `map`, 4 | bettering mirroring the structure in my server-side database? 5 | 6 | ### Answer 7 | 8 | These libraries might be interesting to you: 9 | - [compound](https://github.com/riverford/compound) 10 | - [SubGraph](https://github.com/vimsical/subgraph) 11 | - [pull](https://github.com/juxt/pull) 12 | 13 | See also [this comment](https://github.com/day8/re-frame/issues/304#issuecomment-269620609). 14 | 15 | If you want to go whole hog and ditch `app-db` and embrace DataScript, 16 | then you might find [re-posh](https://github.com/denistakeda/re-posh) interesting. 17 | 18 | 19 | *** 20 | 21 | Up: [FAQ Index](README.md)       22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /docs/FAQs/DoINeedReFrame.md: -------------------------------------------------------------------------------- 1 | ### Question 2 | 3 | Reagent looks terrific. So, why do I need re-frame? What benefit 4 | is there in the extra layers and conceptual overhead it brings? 5 | 6 | ### Answer 7 | 8 | Yes, we agree, Reagent is terrific. We use it enthusiastically ourselves. And, yes, we'd agree that if your application 9 | is small and simple, then standalone Reagent is a reasonable choice. 10 | 11 | But it does only supply the V part of the MVC triad. As your application 12 | gets bigger and more complicated, you *will* need to find solutions to 13 | questions in the M and C realms. 14 | 15 | Questions like "where do I put control logic?". 16 | And, "how do I store and update state?". 17 | And, "how should new websocket packets be communicated with the broader app"? Or GET failures? 18 | And, "how do I put up a spinner 19 | when waiting for CPU intensive computations to run, while allowing the user to press Cancel?" 20 | How do I ensure efficient view updates? How do I write my control logic in a way that's testable? 21 | 22 | These questions accumulate. 23 | 24 | Reagent, by itself, provides little guidance and, so, you'll need to 25 | design your own solutions. Your choices will also accumulate and, 26 | over time, they'll become baked into your codebase. 27 | 28 | Now, any decision which is hard to revisit later is an architectural decision - 29 | "difficult to change later" is pretty much the definition of "architecture". So, 30 | as you proceed, baking your decisions into your codebase, you will be 31 | incrementally growing an architecture. 32 | 33 | So, then, the question is this: is your architecture better than re-frame's? Because 34 | that's what re-frame gives you ... an architecture ... solutions to the 35 | various challenges you'll face when developing your app, and mechanism for implementing 36 | those solutions. 37 | 38 | Now, in response, some will enthusiastically say "yes, I want to grow my own 39 | architecture. I like mine!". And fair enough - it can be an interesting ride! 40 | 41 | Problems arise ONLY when this process is not conscious and purposeful. It is a 42 | credit to Reagent that you can accelerate quickly and get a bunch of enjoyable 43 | early wins. But, equally, that acceleration can have you off the road 44 | in a ditch because of the twists and turns on the way to a larger application. 45 | 46 | I've had many people (20?) privately say to me that's what happened to them. 47 | And that's pretty much the reason for this FAQ - this happens a bit too often 48 | and there's been a bit too much pain. 49 | 50 | So, my advice is ... if your application is a little more complicated, 51 | be sure to make a conscious choice around architecture. Don't think 52 | "Reagent is all I need", because it isn't. One way or 53 | another you'll be using "Reagent + a broader architecture". 54 | 55 | ### Example Choices Made By re-frame 56 | 57 | 1. Events are cardinal. Nothing happens without an event. 58 | 2. Events are data (so they they are loggable, and can be queued, etc). 59 | 3. Events are handled async (a critical decision. Engineered to avoid some `core.async` issues!). 60 | 4. For efficiency, subscriptions (reactions) should be layered and de-duplicated. 61 | 5. Views are never imperative or side effecting (best effort). 62 | 6. Unidirectional data flow only, ever. 63 | 7. Interceptors over middleware. Provide cross cutting concerns like logging and debugging. 64 | 8. Event handlers capture control and contain key code. Ensure purity via coeffects and effects. 65 | 9. All state is stored in one place. 66 | 10. State is committed-to transactionally, never incrementally or piecemeal. 67 | 68 | Hmm. I feel like I'm missing a few, but that's certainly an indicative list. 69 | 70 | re-frame is only about 750 lines of code. So it's value is much more in the honed 71 | choices it embodies (and documents), than the code it provides. 72 | 73 | ### What Reagent Does Provide 74 | 75 | Above I said: 76 | > Reagent, by itself, provides little guidance ... 77 | 78 | which is true but, it does provide useful building blocks. If you do want to create 79 | your own architecture, then be sure to check out Reagent's `track`, `reaction` and `rswap`. 80 | 81 | There's also other Reagent-based architectures like [keechma](https://github.com/keechma/keechma) and 82 | [carry](https://github.com/metametadata/carry) which make different choices - ones which may 83 | better suit your needs. 84 | 85 | *** 86 | 87 | Up: [FAQ Index](README.md)       88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /docs/FAQs/FullStackReframe.md: -------------------------------------------------------------------------------- 1 | ### Question 2 | 3 | I'm interested in taking the re-frame concepts and applying them to 4 | my entire Client/Server stack. 5 | 6 | ### Short Answer 7 | 8 | You'll want to investigate CQRS (vs REST). 9 | 10 | ##### Notes 11 | 12 | 1. Perhaps watch [Bobby Calderwood's video](https://www.youtube.com/watch?v=B1-gS0oEtYc)? 13 | 2. Look at his [reference implementation](https://github.com/capitalone/cqrs-manager-for-distributed-reactive-services) or, perhaps, [this alternative](https://github.com/greywolve/calderwood). 14 | 4. Be aware that "Event Sourcing" often comes along for the ride 15 | with CQRS, but it doesn't have to. It adds complexity (Kafka?). 16 | Don't do it lightly. Maybe just use CQRS without ES? 17 | 5. If you do want Event Sourcing, then Kafka might be your friend, 18 | Greg Young might be your God and [Onyx](https://github.com/onyx-platform/onyx) 19 | may be useful. 20 | 21 | ### Further Related Links 22 | 23 | * Reactive PostgreSQL: 24 | https://yogthos.net/posts/2016-11-05-LuminusPostgresNotifications.html 25 | * Datalog All The Way Down: 26 | https://www.youtube.com/watch?v=aI0zVzzoK_E 27 | 28 | 29 | *** 30 | 31 | Up: [FAQ Index](README.md)       32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /docs/FAQs/GlobalInterceptors.md: -------------------------------------------------------------------------------- 1 | ### Question 2 | 3 | Does re-frame allow me to register global interceptors? Ones which are included 4 | for every event handler? 5 | 6 | ### Short Answer 7 | 8 | No, nothing direct. 9 | 10 | ### Longer Answer 11 | 12 | It's easy to make happen. 13 | 14 | Let's assume you have an interceptor called `omni-ceptor` which you want 15 | automatically added to all your event handlers. 16 | 17 | You'd write a replacement for both `reg-event-db` and `reg-event-fx`, and get 18 | these replacements to automatically add `omni-ceptor` to the interceptor 19 | chain at registration time. 20 | 21 | Here's how to write one of these auto-injecting replacements: 22 | ```clj 23 | (defn my-reg-event-db ;; a replacement for reg-event-db 24 | 25 | ;; 2-arity with no interceptors 26 | ([id handler] 27 | (my-reg-event-db id nil handler)) 28 | 29 | ;; 3-arity with interceptors 30 | ([id interceptors handler] 31 | (re-frame.core/reg-event-db ;; which uses reg-event-db 32 | id 33 | [omni-ceptor interceptors] ;; <-- inject `omni-ceptor` 34 | handler))) 35 | ``` 36 | 37 | NB: did you know that interceptor chains are flattened and nils are removed? 38 | 39 | With this in place, you would always use `my-reg-event-db` 40 | instead of the standard `reg-event-db`: 41 | ```clj 42 | (my-reg-event-db 43 | :event-id 44 | (fn [db v] 45 | ...)) 46 | ``` 47 | 48 | And, hey presto, you'd have your `omni-ceptor` "globally" injected. 49 | 50 | 51 | *** 52 | 53 | Up: [FAQ Index](README.md)       54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /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 | If you want a visual browser of app-db, along with inspecting subpaths of app-db, and diffing changes, use [re-frame-10x](https://github.com/day8/re-frame-10x). 12 | 13 | You are [using cljs-devtools](https://github.com/binaryage/cljs-devtools), right? 14 | If not, stop everything ([unless you are using re-natal](https://github.com/drapanjanas/re-natal/issues/137)) and immediately make that happen. 15 | 16 | ### Better Answer 17 | 18 | Are you sure you need to? 19 | 20 | First, you seldom want to inspect all of `app-db`. 21 | And, second, inspecting via a REPL might be clumsy. 22 | 23 | Instead, you probably want to inspect a part of `app-db`. __And__ you probably want 24 | to inspect it directly in the GUI itself, not off in a REPL. 25 | 26 | Here is a useful technique from @escherize. Add something like this to 27 | the hiccup of your view ... 28 | ```clj 29 | [:pre (with-out-str (pprint @interesting))] 30 | ``` 31 | This assumes that `@interesting` is the value (ratom or subscription) 32 | you want to observe (note the @ in front). 33 | 34 | `pprint` output is nice to read, but not compact. For a more compact view, do this: 35 | ```clj 36 | [:pre (pr-str @some-atom)] ;; NB: using pr-str instead of pprint 37 | ``` 38 | 39 | If you choose to use `pprint` then you'll need to `require` it within the `ns` of your `view.cljs`: 40 | ```clj 41 | [cljs.pprint :refer [pprint]] 42 | ``` 43 | 44 | Finally, combining the short and long answers, you could even do this: 45 | ```clj 46 | [:pre (with-out-str (pprint @re-frame.db/app-db))] ;; see everything! 47 | ``` 48 | or 49 | ```clj 50 | [:pre (with-out-str (pprint (:part @re-frame.db/app-db)))] ;; see a part of it! 51 | ``` 52 | 53 | You definitely have [clj-devtools](https://github.com/binaryage/cljs-devtools) installed now, right? 54 | 55 | ### Other Inspection Tools 56 | 57 | Another very strong tool is [re-Frisk](https://github.com/flexsurfer/re-frisk) which 58 | provides a nice solution for navigating and inspecting your re-frame data structures. 59 | 60 | @yogthos' [json-html library](https://github.com/yogthos/json-html) provides 61 | a slick presentation, at the expense of more screen real estate, and the 62 | need to include specific CSS. 63 | 64 | *** 65 | 66 | Up: [FAQ Index](README.md)       67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /docs/FAQs/LoadOnMount.md: -------------------------------------------------------------------------------- 1 | ### Question 2 | 3 | How do I load the data for a view, on navigation to that view? 4 | 5 | ### Don't Do This 6 | 7 | The broader React community often uses a "load data on mount" approach. They 8 | collocate queries with view components and initiate these queries (via a GET?) 9 | within the View's componentDidMount lifecycle method. And then, later, perhaps they 10 | cleanup/stop any database polling in componentWillUnmount. 11 | 12 | This arrangement is not idiomatic for re-frame. Views are not imperative 13 | and they don't initiate database queries. Instead, views are simply a rendering of the 14 | current application state. 15 | 16 | Please read the further explanation in [PurelyFunctional.tv's writeup](https://purelyfunctional.tv/article/react-vs-re-frame/) under the heading "Reacters load data on mount". 17 | 18 | ### Do This Instead 19 | 20 | With re-frame, "imperative stuff" only ever happens because an event 21 | is dispatched. 22 | 23 | When the user clicks on a button or tab to change what is shown 24 | to them in the UI, an event is dispatched, and it is 25 | the associated event handler which will compute the 26 | effects of the user's request. It might: 27 | 1. change application state so the panel is shown 28 | 2. further change application state so that a "twirly busy" thing is shown 29 | 3. issue a database query or open a websocket 30 | 31 | Also, remember that events should model "user intent", like 32 | "I'd now like to view overdue items". Be sure to never model events like 33 | "load overdue items from database" because that's just a 34 | low level operation performed in the service of fulfilling 35 | the user's intent. 36 | 37 | There's a useful effect handler available for HTTP work: 38 | https://github.com/day8/re-frame-http-fx 39 | 40 | Look at the "Real World App" example for inspiration: 41 | https://github.com/jacekschae/conduit 42 | 43 | ### Do You Have A Redux Background? 44 | 45 | The reminder about the re-frame approach: 46 | 1. re-frame is event driven. It is events which move the system from one state to the next 47 | 2. events cause changes: perhaps to `app-db` or opening a web socket. These are the effects of the event 48 | 3. subscriptions simply deliver data 49 | 4. subscriptions are not imperative. They do not cause things to happen 50 | 5. only events cause things to happen and, even then, only via effects 51 | 6. views simply render the data delivered by subsciptions 52 | 7. views are not imperative. They do not cause things to happen 53 | 8. only events cause things to happen and, even then, only via effects 54 | 55 | 56 | *** 57 | 58 | Up: [FAQ Index](README.md)       59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /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 | ```cljs 7 | :on-click (fn [event] (dispatch [:clicked event])) 8 | ``` 9 | 10 | ### Short Answer 11 | 12 | If you want to `dispatch` a js event object to a re-frame 13 | event handler, you must call `(.persist event)` before the `dispatch`. 14 | React recycles events (using a pool), and re-frame event handlers 15 | run async. [Find out more here](https://facebook.github.io/react/docs/events.html) 16 | 17 | 18 | ### Longer Answer 19 | 20 | It is better to extract the salient details from the event 21 | and `dispatch` them, rather than the entire js event object. When you 22 | `dispatch` pure, simple ClojureScript data (ie. rather than js objects) testing 23 | and debugging will be easier. 24 | 25 | To put this point even more strongly again, think about it like this: 26 | - a DOM `on-click` `callback` might tell us "a button was clicked" 27 | - our application must then interpret that click. The click means 28 | the user wanted to achieve something. They had "intent". 29 | - it is this "intent" which should be captured in the re-frame `event` 30 | which is dispatched. It is this intent which the event handler must 31 | later facilitate. 32 | 33 | 34 | So, in summary, re-frame view functions should transform DOM events 35 | into re-frame `events` which capture user intent: "a button was clicked" 36 | becomes `user wants to delete item with id 42` 37 | 38 | As a result, philosophically, low-level DOM objects have no place in an event. 39 | 40 | 41 | *** 42 | 43 | Up: [FAQ Index](README.md)       44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /docs/FAQs/PollADatabaseEvery60.md: -------------------------------------------------------------------------------- 1 | ### Question 2 | 3 | When the user switches to a particular panel, I'd like to start regularly polling my 4 | backend (database) - say every 60 seconds. And, then later, when the user switches 5 | away from that panel, I want to stop that polling. 6 | 7 | ### First, What Not To Do 8 | 9 | Please be sure to read [How Do I Load On Mount?](LoadOnMount.md) 10 | 11 | ### An Answer 12 | 13 | 14 | We'll create an effect. It will be general in nature. 15 | 16 | It will start and stop the timed/scheduled dispatch of an event. 17 | For this FAQ, 18 | we want an event dispatched every 60 seconds and each event will 19 | trigger a backend poll, but the effect we are about to create 20 | will be useful well beyond this narrow case. 21 | 22 | We'll be creating an `effect` called, say, `:interval`. So, event handlers 23 | will be returning: 24 | ```clj 25 | {:interval } 26 | ``` 27 | So now we design the `` bit. It will be a data format (DSL) which 28 | allows an event handler to start and stop a regular event dispatch. 29 | 30 | To `:start` a regular dispatch, an event handler would return 31 | data in this format: 32 | ```clj 33 | {:interval {:action :start 34 | :id :panel-1-query ;; my id for this (so I can cancel later) 35 | :frequency 60000 ;; how many ms between dispatches 36 | :event [:panel-query 1]}} ;; what to dispatch 37 | ``` 38 | 39 | And to later cancel the regular dispatch, an event handler would return this: 40 | ```clj 41 | {:interval {:action :cancel 42 | :id :panel-1-query}} ;; the id provided to :start 43 | ``` 44 | 45 | With that design work done, let's now implement it by registering an 46 | `effect handler`: 47 | ```clj 48 | (re-frame.core/reg-fx ;; the re-frame API for registering effect handlers 49 | :interval ;; the effect id 50 | (let [live-intervals (atom {})] ;; storage for live intervals 51 | (fn [{:keys [action id frequency event]}] ;; the handler 52 | (if (= action :start) 53 | (swap! live-intervals assoc id (js/setInterval #(dispatch event) frequency))) 54 | (do (js/clearInterval (get @live-intervals id)) 55 | (swap! live-intervals dissoc id)))) 56 | ``` 57 | 58 | You'd probably want a bit more error checking, but that's the (untested) sketch. 59 | 60 | ### A Side Note About Effect Handlers and Figwheel 61 | 62 | [Figwheel](https://github.com/bhauman/lein-figwheel) provides for the hot reloading of code, which 63 | is terrific. 64 | 65 | But, during development, as Figwheel is reloading code, effectful handlers, like the 66 | one above, can be get into a messed up state - existing timers might be lost (and 67 | become never-stoppable). 68 | 69 | Stateful things are grubby in the face of reloading, and all we can do is 70 | try to manage for it as best we can, on a case by case basis. 71 | 72 | One strategy is to put all your grubby effect handlers into their own 73 | separate namespace `effects.cljs` - one that isn't edited often, removing 74 | the trigger for a Figwheel reload. 75 | 76 | OR, you can code defensively for reloading, perhaps like this: 77 | ```clj 78 | (defonce interval-handler ;; notice the use of defonce 79 | (let [live-intervals (atom {})] ;; storage for live intervals 80 | (fn handler [{:keys [action id frequency event]}] ;; the effect handler 81 | (condp = action 82 | :clean (doall ;; <--- new. clean up all existing 83 | (map #(handler {:action :end :id %1}) (keys @live-intervals)) 84 | :start (swap! live-intervals assoc id (js/setInterval #(dispatch event) frequency))) 85 | :end (do (js/clearInterval (get @live-intervals id)) 86 | (swap! live-intervals dissoc id)))) 87 | 88 | ;; when this code is reloaded `:clean` existing intervals 89 | (interval-handler {:action :clean}) 90 | 91 | ;; now register 92 | (re-frame.core/reg-fx ;; the re-frame API for registering effect handlers 93 | :interval ;; the effect id 94 | interval-handler) 95 | ``` 96 | 97 | **Key takeaway:** every effect handler is statefully grubby in its own special way. So you'll have to 98 | come up with strategies to handle Figwheel reloads on a case by case basis. Sometimes 99 | there's no escaping an application restart. 100 | 101 | 102 | *** 103 | 104 | Up: [FAQ Index](README.md)       105 | 106 | -------------------------------------------------------------------------------- /docs/FAQs/README.md: -------------------------------------------------------------------------------- 1 | ## Frequently Asked Questions 2 | 3 | 1. [How can I Inspect app-db?](Inspecting-app-db.md) 4 | 2. [How Do I Load On Mount?](LoadOnMount.md) (hint: you don't) 5 | 2. [Reagent looks terrific. Why do I need re-frame?](DoINeedReFrame.md) 6 | 2. [How do I do full-stack re-frame?](FullStackReframe.md) 7 | 2. [How long after I do a dispatch does the event get handled?](When-Does-Dispatch-Happen.md) 8 | 2. [How can I use a subscription in an Event Handler](UseASubscriptionInAnEventHandler.md) 9 | 2. [How do I use logging method X](Logging.md) 10 | 3. [Dispatched Events Are Null](Null-Dispatched-Events.md) 11 | 4. [Why is re-frame implemented in `.cljc` files](Why-CLJC.md) 12 | 5. [Why do we need to clear the subscription cache when reloading with Figwheel?](Why-Clear-Sub-Cache.md) 13 | 6. [How can I detect exceptions in Event Handlers?](CatchingEventExceptions.md) 14 | 7. [How do I store normalised data in app-db?](DB_Normalisation.md) 15 | 8. [How do I register a global interceptor](GlobalInterceptors.md) 16 | 9. [How do I turn on/off polling a database every 60 secs](PollADatabaseEvery60.md) 17 | 10. [re-frame's uses side-effecting registrations. Should I feel dirty?](ViewsOnGlobalRegistration.md) 18 | 19 | 20 | ## Want To Add An FAQ? 21 | 22 | We'd like that. Please supply a PR. Or just open an issue. Many Thanks!! 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /docs/FAQs/UseASubscriptionInAnEventHandler.md: -------------------------------------------------------------------------------- 1 | ### Question 2 | 3 | How do I access the value of a subscription from within an event handler? 4 | 5 | ### The Wrong Way 6 | 7 | You should NOT do this: 8 | ```clj 9 | (re-frame.core/reg-event-db 10 | :event-id 11 | (fn [db v] 12 | (let [sub-val @(subscribe [:something])] ;; <--- Eeek 13 | ....))) 14 | ``` 15 | 16 | because that `subscribe`: 17 | 1. might create a memory leak (the subscription might not be "freed") 18 | 2. makes the event handler impure (it grabs a global value) 19 | 20 | ### The Better Way 21 | 22 | Instead, the value of a subscription should 23 | be injected into the `coeffects` of that handler via an interceptor. 24 | 25 | A sketch: 26 | ```clj 27 | (re-frame.core/reg-event-fx ;; handler must access coeffects, so use -fx 28 | :event-id 29 | (inject-sub [:query-id :param]) ;; <-- interceptor will inject subscription value into coeffects 30 | (fn [coeffects event] 31 | (let [sub-val (:something coeffects)] ;; obtain subscription value 32 | ....))) 33 | ``` 34 | 35 | Notes: 36 | 1. `inject-sub` is an interceptor which will get the subscription value and add it to coeffects (somehow) 37 | 2. The event handler obtains the value from coeffects 38 | 39 | So, how to write this `inject-sub` interceptor? 40 | 41 | ### Solutions 42 | 43 | re-frame doesn't yet have a builtin `inject-sub` interceptor to do this injection. 44 | 45 | I'd suggest you use this 3rd party library: 46 | https://github.com/vimsical/re-frame-utils/blob/master/src/vimsical/re_frame/cofx/inject.cljc 47 | 48 | 49 | *** 50 | 51 | Up: [FAQ Index](README.md)       52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /docs/FAQs/ViewsOnGlobalRegistration.md: -------------------------------------------------------------------------------- 1 | 2 | ### Question 3 | 4 | I feel offended by re-frame's `reg-*` API. How is it functional to side effect globally? 5 | 6 | ### Background 7 | 8 | A re-frame app is defined collectively by its handlers. As an app boots, calls to registration 9 | functions like `reg-event-db` and `reg-sub` 10 | collectively "build up" an app, infusing it with behaviour and capability. 11 | 12 | Currently, this "building up" process involves the progressive mutation of 13 | a global `registrar` (map) held internally within `re-frame`. 14 | Each registration adds a new entry to this `registrar`. 15 | 16 | How should we analyse this from a functional point of view? 17 | 18 | ### Answer 19 | 20 | There are three ways to view this: 21 | 22 | 1. Egads! Say it isn't true. Mutation of a global? Summon the functional lynch mob! 23 | 24 | 2. In theory, top-level side effects will introduce some pain points, 25 | but re-frame's design represents a conscious decision to trade off functional purity 26 | for simplicity of everyday developer experience. 27 | So, yes, re-frame represents a point in 28 | the possible design space, with associated pros and cons. But the cons tend to be 29 | theoretical and the pros are real. 30 | 31 | 3. Actually, there's no purity problem! As a Clojure program 32 | starts, each `defn` (becomes a `def` which) happily 33 | `interns` a symbol and function in [a map-ish structure](https://clojuredocs.org/clojure.core/ns-interns) representing a `namespace`. 34 | The lynch mob stays home for that. The pitchforks remain in their rack. 35 | `re-frame` handler registration 36 | is the same pattern - an `id` and `handler function` are interned 37 | within a map-ish structure (a `registrar`), once, on program load. 38 | So, if you feel uncomfortable with what re-frame does, you should also feel uncomfortable about using `defn`. 39 | Also, it would be useful to understand 40 | [how you are creating a virtual machine when you program re-frame](https://github.com/day8/re-frame/blob/master/docs/MentalModelOmnibus.md#on-dsls-and-machines) 41 | 42 | 43 | While Point 3 is an interesting perspective to consider, the real discussion should probably be around points 1 and 2: is it a good idea for re-frame to tradeoff purity for simplicity? You can't really judge this 44 | properly until you have used it and experienced the simplicity, and/or found pain points (devcards!). 45 | Many people experience few problems and live happily ever after. For others, the conceptual 46 | distaste is insurmountable and nagging. Like it or hate it, please realise it was a deliberate 47 | and conscious design decision, not some oversight. 48 | 49 | -------- 50 | 51 | Up: [FAQ Index](README.md)       52 | 53 | 54 | 55 | 56 | 57 | ​ 58 | 59 | -- 60 | -------------------------------------------------------------------------------- /docs/FAQs/When-Does-Dispatch-Happen.md: -------------------------------------------------------------------------------- 1 | ### Question 2 | 3 | How long after I call `dispatch` will the event be processed? 4 | 5 | ### Answer 6 | 7 | The answer is "it depends", but [this comment in the code](https://github.com/day8/re-frame/blob/master/src/re_frame/router.cljc#L8-L60) 8 | might provide you the answers you seek. 9 | 10 | 11 | Up: [FAQ Index](README.md)       12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /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/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/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: [App Structure](App-Structure.md)       63 | Up: [Index](README.md)       64 | Next: [Namespaced Keywords](Namespaced-Keywords.md) 65 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ## Introduction 2 | * First, read this repo's [README](../README.md) 3 | * [app-db (Application State)](ApplicationState.md) 4 | * [First Code Walk-Through](CodeWalkthrough.md) 5 | * [Infographic: A re-frame Epoch](AnEpoch.md) 6 | 7 | 8 | ## Dominoes 2 & 3 (event, effect and coeffect handling) 9 | * [Infographic: Event Processing](EventHandlingInfographic.md) 10 | * [Effectful Handlers](EffectfulHandlers.md) 11 | * [Interceptors](Interceptors.md) 12 | * [Effects](Effects.md) 13 | * [Coeffects](Coeffects.md) 14 | 15 | ## Domino 4 (Subscriptions) 16 | 17 | * [Infographic: Subscriptions and The Signal Graph](SubscriptionInfographic.md) 18 | * [Correcting a wrong](SubscriptionsCleanup.md) 19 | * [Flow Mechanics](SubscriptionFlow.md) 20 | 21 | ## Domino 5 (Reagent) 22 | 23 | All the material you need is [here, in Reagent's /doc](https://github.com/reagent-project/reagent/blob/master/doc/README.md). 24 | 25 | ## Deepen/Broaden Your Understanding 26 | * [The API](API.md) 27 | * [An interesting overview of re-frame by purelyfunctional.tv (external link)](https://purelyfunctional.tv/guide/re-frame-building-blocks/) 28 | * [Mental Model Omnibus](MentalModelOmnibus.md) 29 | * [Interesting Resources - including example apps](External-Resources.md) 30 | * [FAQs](FAQs/README.md) 31 | 32 | ## App Structure 33 | * [App Structure](App-Structure.md) 34 | * [On naming things and app-db structure (external link)](https://purelyfunctional.tv/guide/database-structure-in-re-frame/) 35 | * [Navigation](Navigation.md) 36 | * [Namespaced Keywords](Namespaced-Keywords.md) 37 | 38 | ## App Data 39 | * [Loading Initial Data](Loading-Initial-Data.md) 40 | * [Talking To Servers](Talking-To-Servers.md) 41 | * [Subscribing to External Data](Subscribing-To-External-Data.md) 42 | 43 | ## Debugging And Testing 44 | * [Debugging Event Handlers](Debugging-Event-Handlers.md) 45 | * [Debugging](Debugging.md) 46 | * [Testing](Testing.md) 47 | 48 | ## Commercial-Grade Video Training 49 | 50 | * [purelyfunctional.tv](https://purelyfunctional.tv/courses/understanding-re-frame/) 51 | * [Lambda Island Videos](https://lambdaisland.com/collections/react-reagent-re-frame) 52 | 53 | ## Miscellaneous 54 | * [Eek! Performance Problems](Performance-Problems.md) 55 | * [Solve the CPU hog problem](Solve-the-CPU-hog-problem.md) 56 | * [Using Stateful JS Components](Using-Stateful-JS-Components.md) 57 | * [The re-frame Logo](The-re-frame-logo.md) 58 | -------------------------------------------------------------------------------- /docs/SubscriptionInfographic.md: -------------------------------------------------------------------------------- 1 | ## Subscriptions 2 | 3 | A re-frame app is 75% derived data. 4 | 5 | `app-db` is the root, authoritative source of data but radiating 6 | from it is a graph of nodes all computing derived data. Ultimately, the leaf nodes of 7 | this graph are ViewFunctions which compute hiccup (yes, derived data) 8 | which is rendered into the application's UI. But sitting within 9 | the graph, between the root and these leaves, are intermediate 10 | computational nodes supplied by subscriptions which compute 11 | materialised views of the data in `app-db`. 12 | 13 | ## The Domino Narrative 14 | 15 | In terms of the dominos narrative, subscriptions are domino 4, 16 | and the leaf View Functions are domino 5. For tutorial purposes, 17 | we distinguish between them - they serve different purposes - but 18 | they are, conceptually, all nodes in the same graph. 19 | 20 | ## Graph Shapes 21 | 22 | In the simplest version of a graph, subscriptions simply extract 23 | some part of the data in `app-db`, which then flows on into 24 | ViewFunctions unchanged. 25 | 26 | In more complex examples, subscriptions are 27 | layered, with some obtaining data from one or more other 28 | subscriptions, before a ViewFunctions eventually receive 29 | highly processed versions of what's in `app-db`. 30 | 31 | ## The Layers 32 | 33 | The layers in this graph are as follows: 34 | - layer 1 is the root node, `app-db`. 35 | - layer 2 subscriptions extract data directly from `app-db`. 36 | - layer 3 subscriptions obtain data from other subscriptions (not `app-db`), and compute derived data. 37 | - layer 4 the view functions which compute hiccup (more derived data) 38 | 39 | As we'll see soon, there's efficency reasons to distinguish between layer 2 (extractor) 40 | and layer 3 (materialised view). 41 | 42 | ## reg-sub 43 | 44 | Subscription handlers are registered using `reg-sub`. 45 | 46 | But note: just because you register a handler doesn't mean that node exists in 47 | the graph. You are only defining how the node would compute if it was needed. 48 | 49 | Nodes in the signal graph are created and destroyed according to the demands 50 | of (leaf) ViewFunction nodes (layer 4). 51 | 52 | When a ViewFunction (layer 4) uses a subscription, the graph of nodes needed to service 53 | that subscription will be created and, later, when the ViewFunction is destroyed 54 | that part of the graph will also be destroyed (unless used for other purposes). 55 | 56 | ## An Infographic Depiction 57 | 58 | Please read the following infographic carefully 59 | because it contains important notes. 60 | 61 | 62 | 63 | ## Example Use 64 | 65 | To see `reg-sub` used in a real application, please read through the 66 | heavily commented subscription code 67 | [in the todomvc example](https://github.com/day8/re-frame/blob/master/examples/todomvc/src/todomvc/subs.cljs). 68 | 69 | 70 | 71 | *** 72 | 73 | Previous: [Coeffects](Coeffects.md)       74 | Up: [Index](README.md)       75 | Next: [Correcting a wrong](SubscriptionsCleanup.md)       76 | 77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /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/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 insists that @martinklepsch's design is assertively
22 | trying to `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. Which is surely a classic case of premature abstraction
27 | and over engineering. 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 | Yes, its true, their parties are completely awesome, but you will
34 | need someone to bail you out of jail later. 35 | 36 | ![](/images/logo/Genesis.png) 37 | 38 | For the Functional Fundamentalists, a stern bunch, the logo is a
39 | flowing poststructuralist rebuttal of OO's vowel duplication and
40 | horizontal adjacency. Their alternative approach, FF, is fine, apparently,
41 | because "everyone loves a fricative". 42 | 43 | For his part, @martinklepsch has never confirmed any theory, teasing
44 | us instead with coded clues like "Will you please stop emailing me"
45 | and "Why did you say I hit a horse?". 46 | 47 | ### Assets Where? 48 | 49 | Within this repo, look in `/images/logo/` 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /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 | ```clj 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 | ;; Note: @pos is a map here, so it gets passed as props. 58 | ;; Non-props values can be accessed via (reagent/argv comp) 59 | [gmap-inner @pos]))) 60 | ``` 61 | 62 | 63 | Notes: 64 | - `gmap-outer` obtains data via a subscription. It is quite simple - trivial almost. 65 | - 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) 66 | - 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. 67 | - when the inner component is given new props, its entire set of lifecycle functions will be engaged. 68 | - 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. 69 | - but this inner component has other lifecycle functions and this is where the real work is done. 70 | - 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. 71 | - the props passed (in this case `@pos`) in must be a map, otherwise `(reagent/props comp)` will return nil. 72 | 73 | ### Pattern Discovery 74 | 75 | This pattern has been independently discovered by many. To my knowledge, 76 | [this description of the Container/Component pattern](https://medium.com/@learnreact/container-components-c0e67432e005#.3ic1uipvu) 77 | is the first time it was written up. 78 | 79 | ### Code Credit 80 | 81 | The example gmaps code above was developed by @jhchabran in this gist: 82 | https://gist.github.com/jhchabran/e09883c3bc1b703a224d#file-2_google_map-cljs 83 | 84 | ### D3 Examples 85 | 86 | D3 (from @zachcp): 87 | - Blog Post: http://zachcp.org/blog/2015/reagent-d3/ 88 | - Code: https://github.com/zachcp/simplecomponent 89 | - Example: http://zachcp.github.io/simplecomponent/ 90 | 91 | RID3, a reagent interface to D3 92 | - Repo: https://github.com/gadfly361/rid3 93 | - Demo: https://rawgit.com/gadfly361/rid3/master/dev-resources/public/examples.html 94 | 95 | ### JS Interop 96 | 97 | You'll probably need to know how to do interop with js: 98 | http://www.spacjer.com/blog/2014/09/12/clojurescript-javascript-interop/ 99 | 100 | Perhaps use this library to make it even easier: 101 | https://github.com/binaryage/cljs-oops 102 | 103 | ### Advanced Lifecycle Methods 104 | 105 | If you mess around with lifecycle methods, you'll probably want to read Martin's explanations: 106 | https://www.martinklepsch.org/posts/props-children-and-component-lifecycle-in-reagent.html 107 | 108 | 109 | 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /docs/images/Readme/6dominoes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/docs/images/Readme/6dominoes.png -------------------------------------------------------------------------------- /docs/images/Readme/Dominoes-small.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/docs/images/Readme/Dominoes-small.jpg -------------------------------------------------------------------------------- /docs/images/Readme/Dominoes.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/docs/images/Readme/Dominoes.jpg -------------------------------------------------------------------------------- /docs/images/Readme/todolist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/docs/images/Readme/todolist.png -------------------------------------------------------------------------------- /docs/images/epoch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/docs/images/epoch.png -------------------------------------------------------------------------------- /docs/images/event-handlers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/docs/images/event-handlers.png -------------------------------------------------------------------------------- /docs/images/example_app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/docs/images/example_app.png -------------------------------------------------------------------------------- /docs/images/logo/Genesis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/docs/images/logo/Genesis.png -------------------------------------------------------------------------------- /docs/images/logo/Guggenheim.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/docs/images/logo/Guggenheim.jpg -------------------------------------------------------------------------------- /docs/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 | -------------------------------------------------------------------------------- /docs/images/logo/frame_1024w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/docs/images/logo/frame_1024w.png -------------------------------------------------------------------------------- /docs/images/logo/re-frame-logo.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/docs/images/logo/re-frame-logo.sketch -------------------------------------------------------------------------------- /docs/images/logo/re-frame_128w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/docs/images/logo/re-frame_128w.png -------------------------------------------------------------------------------- /docs/images/logo/re-frame_256w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/docs/images/logo/re-frame_256w.png -------------------------------------------------------------------------------- /docs/images/logo/re-frame_512w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/docs/images/logo/re-frame_512w.png -------------------------------------------------------------------------------- /docs/images/mental-model-omnibus.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/docs/images/mental-model-omnibus.jpg -------------------------------------------------------------------------------- /docs/images/scale-changes-everything.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/docs/images/scale-changes-everything.jpg -------------------------------------------------------------------------------- /docs/images/subscriptions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/docs/images/subscriptions.png -------------------------------------------------------------------------------- /docs/images/the-water-cycle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/docs/images/the-water-cycle.png -------------------------------------------------------------------------------- /docs/styles/website.css: -------------------------------------------------------------------------------- 1 | /* .hljs-symbol { */ 2 | /* color: #AD81FF !important; */ 3 | /* } */ 4 | 5 | /* .hljs-name { */ 6 | /* color: #67D8EE !important; */ 7 | /* } */ 8 | 9 | /* .hljs-builtin-name { */ 10 | /* color: #A6E131; */ 11 | /* } */ 12 | .markdown-section > pre { 13 | color: #657b83 !important; 14 | background: #fdf6e3 !important; 15 | } 16 | .hljs { 17 | display: block; 18 | overflow-x: auto; 19 | padding: 0.5em; 20 | background: #fdf6e3; 21 | color: #657b83; 22 | } 23 | 24 | .hljs-comment, 25 | .hljs-quote { 26 | color: #93a1a1; 27 | } 28 | 29 | /* Solarized Green */ 30 | .hljs-keyword, 31 | .hljs-selector-tag, 32 | .hljs-addition { 33 | color: #859900; 34 | } 35 | 36 | /* Solarized Cyan */ 37 | .hljs-number, 38 | .hljs-string, 39 | .hljs-meta .hljs-meta-string, 40 | .hljs-literal, 41 | .hljs-doctag, 42 | .hljs-regexp { 43 | color: #2aa198; 44 | } 45 | 46 | /* Solarized Blue */ 47 | .hljs-title, 48 | .hljs-section, 49 | .hljs-name, 50 | .hljs-selector-id, 51 | .hljs-selector-class { 52 | color: #268bd2; 53 | } 54 | 55 | /* Solarized Yellow */ 56 | .hljs-attribute, 57 | .hljs-attr, 58 | .hljs-variable, 59 | .hljs-template-variable, 60 | .hljs-class .hljs-title, 61 | .hljs-type { 62 | color: #b58900; 63 | } 64 | 65 | /* Solarized Orange */ 66 | .hljs-symbol, 67 | .hljs-bullet, 68 | .hljs-subst, 69 | .hljs-meta, 70 | .hljs-meta .hljs-keyword, 71 | .hljs-selector-attr, 72 | .hljs-selector-pseudo, 73 | .hljs-link { 74 | color: #cb4b16; 75 | } 76 | 77 | /* Solarized Red */ 78 | .hljs-built_in, 79 | .hljs-deletion { 80 | color: #dc322f; 81 | } 82 | 83 | .hljs-formula { 84 | background: #eee8d5; 85 | } 86 | 87 | .hljs-emphasis { 88 | font-style: italic; 89 | } 90 | 91 | .hljs-strong { 92 | font-weight: bold; 93 | } 94 | -------------------------------------------------------------------------------- /examples/simple/.gitignore: -------------------------------------------------------------------------------- 1 | .shadow-cljs/ 2 | package-lock.json 3 | package.json 4 | shadow-cljs.edn 5 | -------------------------------------------------------------------------------- /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/simple/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, shadow watch client`" to compile the app and start up shadow-cljs hot-reloading 18 | 5. Wait for the compile to finish. At a minumum, that might take 15 seconds. But it can take more like 60 seconds if you are new to ClojureScript and various caches are empty. Eventually you should see `[:client] Build Completed (... stats ...)` 19 | 6. Open `http://localhost:8280/example.html` to see the app 20 | 21 | While step 4 is running, any changes you make to the ClojureScript 22 | source files (in `src`) will be re-compiled and reflected in the running 23 | page immediately. 24 | 25 | ### Production Version 26 | 27 | Run "`lein do clean, shadow release client`" to compile an optimised 28 | version, and then open `resources/public/example.html` in a browser. 29 | -------------------------------------------------------------------------------- /examples/simple/project.clj: -------------------------------------------------------------------------------- 1 | (defproject simple "lein-git-inject/version" 2 | 3 | :dependencies [[org.clojure/clojure "1.10.1"] 4 | [org.clojure/clojurescript "1.10.597" 5 | :exclusions [com.google.javascript/closure-compiler-unshaded 6 | org.clojure/google-closure-library 7 | org.clojure/google-closure-library-third-party]] 8 | [thheller/shadow-cljs "2.8.83"] 9 | [reagent "0.9.1"] 10 | [re-frame "RELEASE"]] 11 | 12 | :plugins [[day8/lein-git-inject "0.0.11"] 13 | [lein-shadow "0.1.7"]] 14 | 15 | :middleware [leiningen.git-inject/middleware] 16 | 17 | :clean-targets ^{:protect false} [:target-path 18 | "shadow-cljs.edn" 19 | "package.json" 20 | "package-lock.json" 21 | "resources/public/js"] 22 | 23 | :shadow-cljs {:nrepl {:port 8777} 24 | 25 | :builds {:client {:target :browser 26 | :output-dir "resources/public/js" 27 | :modules {:client {:init-fn simple.core/run}} 28 | :devtools {:http-root "resources/public" 29 | :http-port 8280}}}} 30 | 31 | :aliases {"dev-auto" ["shadow" "watch" "client"]}) 32 | -------------------------------------------------------------------------------- /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 |

Reframe simple example app – see README.md

11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/simple/src/deps.cljs: -------------------------------------------------------------------------------- 1 | {:npm-dev-deps {"shadow-cljs" "2.8.83"}} 2 | -------------------------------------------------------------------------------- /examples/simple/src/simple/core.cljs: -------------------------------------------------------------------------------- 1 | (ns simple.core 2 | (:require [reagent.core :as reagent] 3 | [re-frame.core :as rf] 4 | [clojure.string :as str])) 5 | 6 | ;; A detailed walk-through of this source code is provided in the docs: 7 | ;; https://github.com/day8/re-frame/blob/master/docs/CodeWalkthrough.md 8 | 9 | ;; -- Domino 1 - Event Dispatch ----------------------------------------------- 10 | 11 | (defn dispatch-timer-event 12 | [] 13 | (let [now (js/Date.)] 14 | (rf/dispatch [:timer now]))) ;; <-- dispatch used 15 | 16 | ;; Call the dispatching function every second. 17 | ;; `defonce` is like `def` but it ensures only one instance is ever 18 | ;; created in the face of figwheel hot-reloading of this file. 19 | (defonce do-timer (js/setInterval dispatch-timer-event 1000)) 20 | 21 | 22 | ;; -- Domino 2 - Event Handlers ----------------------------------------------- 23 | 24 | (rf/reg-event-db ;; sets up initial application state 25 | :initialize ;; usage: (dispatch [:initialize]) 26 | (fn [_ _] ;; the two parameters are not important here, so use _ 27 | {:time (js/Date.) ;; What it returns becomes the new application state 28 | :time-color "#f88"})) ;; so the application state will initially be a map with two keys 29 | 30 | 31 | (rf/reg-event-db ;; usage: (dispatch [:time-color-change 34562]) 32 | :time-color-change ;; dispatched when the user enters a new colour into the UI text field 33 | (fn [db [_ new-color-value]] ;; -db event handlers given 2 parameters: current application state and event (a vector) 34 | (assoc db :time-color new-color-value))) ;; compute and return the new application state 35 | 36 | 37 | (rf/reg-event-db ;; usage: (dispatch [:timer a-js-Date]) 38 | :timer ;; every second an event of this kind will be dispatched 39 | (fn [db [_ new-time]] ;; note how the 2nd parameter is destructured to obtain the data value 40 | (assoc db :time new-time))) ;; compute and return the new application state 41 | 42 | 43 | ;; -- Domino 4 - Query ------------------------------------------------------- 44 | 45 | (rf/reg-sub 46 | :time 47 | (fn [db _] ;; db is current app state. 2nd unused param is query vector 48 | (:time db))) ;; return a query computation over the application state 49 | 50 | (rf/reg-sub 51 | :time-color 52 | (fn [db _] 53 | (:time-color db))) 54 | 55 | 56 | ;; -- Domino 5 - View Functions ---------------------------------------------- 57 | 58 | (defn clock 59 | [] 60 | [:div.example-clock 61 | {:style {:color @(rf/subscribe [:time-color])}} 62 | (-> @(rf/subscribe [:time]) 63 | .toTimeString 64 | (str/split " ") 65 | first)]) 66 | 67 | (defn color-input 68 | [] 69 | [:div.color-input 70 | "Time color: " 71 | [:input {:type "text" 72 | :value @(rf/subscribe [:time-color]) 73 | :on-change #(rf/dispatch [:time-color-change (-> % .-target .-value)])}]]) ;; <--- 74 | 75 | (defn ui 76 | [] 77 | [:div 78 | [:h1 "Hello world, it is now"] 79 | [clock] 80 | [color-input]]) 81 | 82 | ;; -- Entry Point ------------------------------------------------------------- 83 | 84 | (defn render 85 | [] 86 | (reagent/render [ui] 87 | (js/document.getElementById "app"))) 88 | 89 | (defn ^:dev/after-load clear-cache-and-render! 90 | [] 91 | ;; The `:dev/after-load` metadata causes this function to be called 92 | ;; after shadow-cljs hot-reloads code. We force a UI update by clearing 93 | ;; the Reframe subscription cache. 94 | (rf/clear-subscription-cache!) 95 | (render)) 96 | 97 | (defn run 98 | [] 99 | (rf/dispatch-sync [:initialize]) ;; put a value into application state 100 | (render) ;; mount the application's ui into '
' 101 | ) 102 | -------------------------------------------------------------------------------- /examples/todomvc/.gitignore: -------------------------------------------------------------------------------- 1 | resources/public/js 2 | .shadow-cljs/ 3 | package-lock.json 4 | package.json 5 | shadow-cljs.edn 6 | -------------------------------------------------------------------------------- /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 | But this is NOT your normal, lean and minimal todomvc implementation, 6 | geared towards showing how easily re-frame can implement the challenge. 7 | 8 | Instead, this todomvc example has evolved into more of a teaching tool 9 | and we've thrown in more advanced re-frame features which are not 10 | really required to get the job done. So lean and minimal is no longer a goal. 11 | 12 | 13 | ## Setup And Run 14 | 15 | 1. Install [Leiningen](http://leiningen.org/) (plus Java). 16 | 17 | 2. Get the re-frame repo 18 | ``` 19 | git clone https://github.com/day8/re-frame.git 20 | ``` 21 | 22 | 3. cd to the right example directory 23 | ``` 24 | cd re-frame/examples/todomvc 25 | ``` 26 | 27 | 4. Clean build 28 | ``` 29 | lein do clean, shadow watch client 30 | ``` 31 | 32 | 5. Wait for step 4 to do the compile, and then run the built app: 33 | ``` 34 | open http://localhost:8280 35 | ``` 36 | 37 | 38 | ## Compile an optimised version 39 | 40 | 1. Compile 41 | ``` 42 | lein do clean, shadow release client 43 | ``` 44 | 45 | 2. Open the following in your browser 46 | ``` 47 | resources/public/index.html 48 | ``` 49 | 50 | 51 | ## Exploring The Code 52 | 53 | From the re-frame readme: 54 | ``` 55 | To build a re-frame app, you: 56 | - design your app's data structure (data layer) 57 | - write and register subscription functions (query layer) 58 | - write Reagent component functions (view layer) 59 | - write and register event handler functions (control layer and/or state transition layer) 60 | ``` 61 | 62 | In `src`, there's a matching set of files (each small): 63 | ``` 64 | src 65 | ├── core.cljs <--- entry point, plus history 66 | ├── db.cljs <--- data related (data layer) 67 | ├── subs.cljs <--- subscription handlers (query layer) 68 | ├── views.cljs <--- reagent components (view layer) 69 | └── events.cljs <--- event handlers (control/update layer) 70 | ``` 71 | 72 | ## Further Notes 73 | 74 | The [official reagent example](https://github.com/reagent-project/reagent/tree/master/examples/todomvc). 75 | -------------------------------------------------------------------------------- /examples/todomvc/project.clj: -------------------------------------------------------------------------------- 1 | (defproject todomvc-re-frame "lein-git-inject/version" 2 | 3 | :dependencies [[org.clojure/clojure "1.10.1"] 4 | [org.clojure/clojurescript "1.10.597" 5 | :exclusions [com.google.javascript/closure-compiler-unshaded 6 | org.clojure/google-closure-library 7 | org.clojure/google-closure-library-third-party]] 8 | [thheller/shadow-cljs "2.8.83"] 9 | [reagent "0.9.1"] 10 | [re-frame "RELEASE"] 11 | [binaryage/devtools "0.9.10"] 12 | [clj-commons/secretary "1.2.4"] 13 | [day8.re-frame/tracing "0.5.3"]] 14 | 15 | :plugins [[day8/lein-git-inject "0.0.11"] 16 | [lein-shadow "0.1.7"]] 17 | 18 | :middleware [leiningen.git-inject/middleware] 19 | 20 | :clean-targets ^{:protect false} [:target-path 21 | "shadow-cljs.edn" 22 | "package.json" 23 | "package-lock.json" 24 | "resources/public/js"] 25 | 26 | :shadow-cljs {:nrepl {:port 8777} 27 | 28 | :builds {:client {:target :browser 29 | :output-dir "resources/public/js" 30 | :modules {:client {:init-fn todomvc.core/main}} 31 | :devtools {:http-root "resources/public" 32 | :http-port 8280}}}} 33 | 34 | :aliases {"dev-auto" ["shadow" "watch" "client"]}) 35 | -------------------------------------------------------------------------------- /examples/todomvc/resources/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Reframe Todomvc 6 | 7 | 8 | 9 |
10 |

Reframe Todomvc example app – see README.md

11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/todomvc/src/deps.cljs: -------------------------------------------------------------------------------- 1 | {:npm-dev-deps {"shadow-cljs" "2.8.83"}} 2 | -------------------------------------------------------------------------------- /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 :as rf :refer [dispatch dispatch-sync]] 6 | [secretary.core :as secretary] 7 | [todomvc.events] ;; These two are only required to make the compiler 8 | [todomvc.subs] ;; load them (see docs/App-Structure.md) 9 | [todomvc.views] 10 | [devtools.core :as devtools]) 11 | (:import [goog History] 12 | [goog.history EventType])) 13 | 14 | 15 | ;; -- Debugging aids ---------------------------------------------------------- 16 | (devtools/install!) ;; we love https://github.com/binaryage/cljs-devtools 17 | (enable-console-print!) ;; so that println writes to `console.log` 18 | 19 | 20 | ;; Put an initial value into app-db. 21 | ;; The event handler for `:initialise-db` can be found in `events.cljs` 22 | ;; Using the sync version of dispatch means that value is in 23 | ;; place before we go onto the next step. 24 | (dispatch-sync [:initialise-db]) 25 | 26 | ;; -- Routes and History ------------------------------------------------------ 27 | ;; Although we use the secretary library below, that's mostly a historical 28 | ;; accident. You might also consider using: 29 | ;; - https://github.com/DomKM/silk 30 | ;; - https://github.com/juxt/bidi 31 | ;; We don't have a strong opinion. 32 | ;; 33 | (defroute "/" [] (dispatch [:set-showing :all])) 34 | (defroute "/:filter" [filter] (dispatch [:set-showing (keyword filter)])) 35 | 36 | (defonce history 37 | (doto (History.) 38 | (events/listen EventType.NAVIGATE 39 | (fn [event] (secretary/dispatch! (.-token event)))) 40 | (.setEnabled true))) 41 | 42 | 43 | ;; -- Entry Point ------------------------------------------------------------- 44 | 45 | (defn render 46 | [] 47 | ;; Render the UI into the HTML's
element 48 | ;; The view function `todomvc.views/todo-app` is the 49 | ;; root view for the entire UI. 50 | (reagent/render [todomvc.views/todo-app] 51 | (.getElementById js/document "app"))) 52 | 53 | (defn ^:dev/after-load clear-cache-and-render! 54 | [] 55 | ;; The `:dev/after-load` metadata causes this function to be called 56 | ;; after shadow-cljs hot-reloads code. We force a UI update by clearing 57 | ;; the Reframe subscription cache. 58 | (rf/clear-subscription-cache!) 59 | (render)) 60 | 61 | (defn ^:export main 62 | [] 63 | (render)) 64 | -------------------------------------------------------------------------------- /examples/todomvc/src/todomvc/db.cljs: -------------------------------------------------------------------------------- 1 | (ns todomvc.db 2 | (:require [cljs.reader] 3 | [cljs.spec.alpha :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: 42 | ;; 1. `core.cljs` for "(dispatch-sync [:initialise-db])" 43 | ;; 2. `events.cljs` for the registration of :initialise-db handler 44 | ;; 45 | 46 | (def default-db ;; what gets put into app-db by default. 47 | {:todos (sorted-map) ;; an empty list of todos. Use the (int) :id as the key 48 | :showing :all}) ;; show all todos 49 | 50 | 51 | ;; -- Local Storage ---------------------------------------------------------- 52 | ;; 53 | ;; Part of the todomvc challenge is to store todos in LocalStorage, and 54 | ;; on app startup, reload the todos from when the program was last run. 55 | ;; But the challenge stipulates to NOT load the setting for the "showing" 56 | ;; filter. Just the todos. 57 | ;; 58 | 59 | (def ls-key "todos-reframe") ;; localstore key 60 | 61 | (defn todos->local-store 62 | "Puts todos into localStorage" 63 | [todos] 64 | (.setItem js/localStorage ls-key (str todos))) ;; sorted-map written as an EDN map 65 | 66 | 67 | ;; -- cofx Registrations ----------------------------------------------------- 68 | 69 | ;; Use `reg-cofx` to register a "coeffect handler" which will inject the todos 70 | ;; stored in localstore. 71 | ;; 72 | ;; To see it used, look in `events.cljs` at the event handler for `:initialise-db`. 73 | ;; That event handler has the interceptor `(inject-cofx :local-store-todos)` 74 | ;; The function registered below will be used to fulfill that request. 75 | ;; 76 | ;; We must supply a `sorted-map` but in LocalStore it is stored as a `map`. 77 | ;; 78 | (re-frame/reg-cofx 79 | :local-store-todos 80 | (fn [cofx _] 81 | ;; put the localstore todos into the coeffect under :local-store-todos 82 | (assoc cofx :local-store-todos 83 | ;; read in todos from localstore, and process into a sorted map 84 | (into (sorted-map) 85 | (some->> (.getItem js/localStorage ls-key) 86 | (cljs.reader/read-string) ;; EDN map -> map 87 | ))))) 88 | -------------------------------------------------------------------------------- /examples/todomvc/src/todomvc/views.cljs: -------------------------------------------------------------------------------- 1 | (ns todomvc.views 2 | (:require [reagent.core :as reagent] 3 | [re-frame.core :refer [subscribe dispatch]] 4 | [clojure.string :as str])) 5 | 6 | 7 | (defn todo-input [{:keys [title on-save on-stop]}] 8 | (let [val (reagent/atom title) 9 | stop #(do (reset! val "") 10 | (when on-stop (on-stop))) 11 | save #(let [v (-> @val str str/trim)] 12 | (on-save v) 13 | (stop))] 14 | (fn [props] 15 | [:input (merge (dissoc props :on-save :on-stop :title) 16 | {:type "text" 17 | :value @val 18 | :auto-focus true 19 | :on-blur save 20 | :on-change #(reset! val (-> % .-target .-value)) 21 | :on-key-down #(case (.-which %) 22 | 13 (save) 23 | 27 (stop) 24 | nil)})]))) 25 | 26 | 27 | (defn todo-item 28 | [] 29 | (let [editing (reagent/atom false)] 30 | (fn [{:keys [id done title]}] 31 | [:li {:class (str (when done "completed ") 32 | (when @editing "editing"))} 33 | [:div.view 34 | [:input.toggle 35 | {:type "checkbox" 36 | :checked done 37 | :on-change #(dispatch [:toggle-done id])}] 38 | [:label 39 | {:on-double-click #(reset! editing true)} 40 | title] 41 | [:button.destroy 42 | {:on-click #(dispatch [:delete-todo id])}]] 43 | (when @editing 44 | [todo-input 45 | {:class "edit" 46 | :title title 47 | :on-save #(if (seq %) 48 | (dispatch [:save id %]) 49 | (dispatch [:delete-todo id])) 50 | :on-stop #(reset! editing false)}])]))) 51 | 52 | 53 | (defn task-list 54 | [] 55 | (let [visible-todos @(subscribe [:visible-todos]) 56 | all-complete? @(subscribe [:all-complete?])] 57 | [:section#main 58 | [:input#toggle-all 59 | {:type "checkbox" 60 | :checked all-complete? 61 | :on-change #(dispatch [:complete-all-toggle])}] 62 | [:label 63 | {:for "toggle-all"} 64 | "Mark all as complete"] 65 | [:ul#todo-list 66 | (for [todo visible-todos] 67 | ^{:key (:id todo)} [todo-item todo])]])) 68 | 69 | 70 | (defn footer-controls 71 | [] 72 | (let [[active done] @(subscribe [:footer-counts]) 73 | showing @(subscribe [:showing]) 74 | a-fn (fn [filter-kw txt] 75 | [:a {:class (when (= filter-kw showing) "selected") 76 | :href (str "#/" (name filter-kw))} txt])] 77 | [:footer#footer 78 | [:span#todo-count 79 | [:strong active] " " (case active 1 "item" "items") " left"] 80 | [:ul#filters 81 | [:li (a-fn :all "All")] 82 | [:li (a-fn :active "Active")] 83 | [:li (a-fn :done "Completed")]] 84 | (when (pos? done) 85 | [:button#clear-completed {:on-click #(dispatch [:clear-completed])} 86 | "Clear completed"])])) 87 | 88 | 89 | (defn task-entry 90 | [] 91 | [:header#header 92 | [:h1 "todos"] 93 | [todo-input 94 | {:id "new-todo" 95 | :placeholder "What needs to be done?" 96 | :on-save #(when (seq %) 97 | (dispatch [:add-todo %]))}]]) 98 | 99 | 100 | (defn todo-app 101 | [] 102 | [:div 103 | [:section#todoapp 104 | [task-entry] 105 | (when (seq @(subscribe [:todos])) 106 | [task-list]) 107 | [footer-controls]] 108 | [:footer#info 109 | [:p "Double-click to edit a todo"]]]) 110 | -------------------------------------------------------------------------------- /images/Readme/6dominoes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/images/Readme/6dominoes.png -------------------------------------------------------------------------------- /images/Readme/Dominoes-small.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/images/Readme/Dominoes-small.jpg -------------------------------------------------------------------------------- /images/Readme/Dominoes.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/images/Readme/Dominoes.jpg -------------------------------------------------------------------------------- /images/Readme/todolist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/images/Readme/todolist.png -------------------------------------------------------------------------------- /images/epoch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/images/epoch.png -------------------------------------------------------------------------------- /images/event-handlers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/images/event-handlers.png -------------------------------------------------------------------------------- /images/example_app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/images/example_app.png -------------------------------------------------------------------------------- /images/logo/Genesis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/images/logo/Genesis.png -------------------------------------------------------------------------------- /images/logo/Guggenheim.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/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/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/images/logo/frame_1024w.png -------------------------------------------------------------------------------- /images/logo/re-frame-logo.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/images/logo/re-frame-logo.sketch -------------------------------------------------------------------------------- /images/logo/re-frame_128w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/images/logo/re-frame_128w.png -------------------------------------------------------------------------------- /images/logo/re-frame_256w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/images/logo/re-frame_256w.png -------------------------------------------------------------------------------- /images/logo/re-frame_512w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/images/logo/re-frame_512w.png -------------------------------------------------------------------------------- /images/mental-model-omnibus.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/images/mental-model-omnibus.jpg -------------------------------------------------------------------------------- /images/scale-changes-everything.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/images/scale-changes-everything.jpg -------------------------------------------------------------------------------- /images/subscriptions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/images/subscriptions.png -------------------------------------------------------------------------------- /images/the-water-cycle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextjournal/freerange/8cf68c30722a4c6f8f948a134c900d7a656ecad4/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: ['ChromeHeadless'], 8 | basePath: './', 9 | files: [ 10 | root + '/test.js' 11 | ], 12 | plugins: [ 13 | 'karma-cljs-test', 14 | 'karma-chrome-launcher', 15 | 'karma-junit-reporter' 16 | ], 17 | colors: true, 18 | logLevel: config.LOG_INFO, 19 | client: { 20 | args: ['shadow.test.karma.init'], 21 | singleRun: true 22 | }, 23 | 24 | // the default configuration 25 | junitReporter: { 26 | outputDir: junitOutputDir + '/karma', // results will be saved as outputDir/browserName.xml 27 | outputFile: undefined, // if included, results will be saved as outputDir/browserName/outputFile 28 | suite: '' // suite will become the package name attribute in xml testsuite element 29 | } 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /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 re-frame "lein-git-inject/version" 2 | :description "A ClojureScript MVC-like Framework For Writing SPAs Using Reagent." 3 | :url "https://github.com/day8/re-frame.git" 4 | :license {:name "MIT"} 5 | 6 | :dependencies [[org.clojure/clojure "1.10.1" :scope "provided"] 7 | [org.clojure/clojurescript "1.10.597" :scope "provided" 8 | :exclusions [com.google.javascript/closure-compiler-unshaded 9 | org.clojure/google-closure-library 10 | org.clojure/google-closure-library-third-party]] 11 | [thheller/shadow-cljs "2.8.83" :scope "provided"] 12 | [reagent "0.9.1"] 13 | [net.cgrand/macrovich "0.2.1"] 14 | [org.clojure/tools.logging "0.4.1"]] 15 | 16 | :plugins [[day8/lein-git-inject "0.0.11"] 17 | [lein-shadow "0.1.7"]] 18 | 19 | :middleware [leiningen.git-inject/middleware] 20 | 21 | :git-inject {:version-pattern #"v(\d+\.\d+\.\d+.*)"} 22 | 23 | :profiles {:debug {:debug true} 24 | :dev {:dependencies [[binaryage/devtools "0.9.11"]] 25 | :plugins [[lein-ancient "0.6.15"] 26 | [lein-shell "0.5.0"]]}} 27 | 28 | :clean-targets [:target-path "run/compiled"] 29 | 30 | :resource-paths ["run/resources"] 31 | :jvm-opts ["-Xmx1g"] 32 | :source-paths ["src"] 33 | :test-paths ["test"] 34 | 35 | :shell {:commands {"open" {:windows ["cmd" "/c" "start"] 36 | :macosx "open" 37 | :linux "xdg-open"}}} 38 | 39 | :deploy-repositories [["clojars" {:sign-releases false 40 | :url "https://clojars.org/repo" 41 | :username :env/CLOJARS_USERNAME 42 | :password :env/CLOJARS_PASSWORD}]] 43 | 44 | :release-tasks [["deploy" "clojars"]] 45 | 46 | :shadow-cljs {:nrepl {:port 8777} 47 | 48 | :builds {:browser-test 49 | {:target :browser-test 50 | :ns-regexp "re-frame\\..*-test$" 51 | :test-dir "run/compiled/browser/test" 52 | :compiler-options {:pretty-print true 53 | :external-config {:devtools/config {:features-to-install [:formatters :hints]}}} 54 | :devtools {:http-port 3449 55 | :http-root "run/compiled/browser/test" 56 | :preloads [devtools.preload]}} 57 | 58 | :karma-test 59 | {:target :karma 60 | :ns-regexp "re-frame\\..*-test$" 61 | :output-to "run/compiled/karma/test/test.js" 62 | :compiler-options {:pretty-print true 63 | :closure-defines {re-frame.trace.trace-enabled? true}}}}} 64 | 65 | :aliases {"test-once" ["do" "clean," "shadow" "compile" "browser-test," "shell" "open" "run/compiled/browser/test/index.html"] 66 | "test-auto" ["do" "clean," "shadow" "watch" "browser-test,"] 67 | "karma-once" ["do" 68 | ["clean"] 69 | ["shadow" "compile" "karma-test"] 70 | ["shell" "karma" "start" "--single-run" "--reporters" "junit,dots"]] 71 | "karma-auto" ["do" "clean," "shadow" "watch" "karma-test,"] 72 | ;; NOTE: In order to compile docs you would need to install 73 | ;; gitbook-cli(2.3.2) utility globaly using npm or yarn 74 | "docs-serve" ^{:doc "Runs the development server of docs with live reloading"} ["shell" "gitbook" "serve" "./" "./build/re-frame/"] 75 | "docs-build" ^{:doc "Builds the HTML version of docs"} ["shell" "gitbook" "build" "./" "./build/re-frame/"] 76 | ;; NOTE: Calibre and svgexport(0.3.2) are needed to build below 77 | ;; formats of docs. Install svgexpor3t using npm or yarn. 78 | "docs-pdf" ^{:doc "Builds the PDF version of docs"} 79 | ["do" 80 | ["shell" "mkdir" "-p" "./build/"] 81 | ["shell" "gitbook" "pdf" "./" "./build/re-frame.pdf"]] 82 | 83 | "docs-mobi" ^{:doc "Builds the MOBI version of docs"} 84 | ["do" 85 | ["shell" "mkdir" "-p" "./build/"] 86 | ["shell" "gitbook" "mobi" "./" "./build/re-frame.mobi"]] 87 | 88 | "docs-epub" ^{:doc "Builds the EPUB version of docs"} 89 | ["do" 90 | ["shell" "mkdir" "-p" "./build/"] 91 | ["shell" "gitbook" "epub" "./" "./build/re-frame.epub"]]}) 92 | -------------------------------------------------------------------------------- /src/deps.cljs: -------------------------------------------------------------------------------- 1 | {:npm-dev-deps {"shadow-cljs" "2.8.83" 2 | "karma" "4.4.1" 3 | "karma-chrome-launcher" "3.1.0" 4 | "karma-cljs-test" "0.1.0" 5 | "karma-junit-reporter" "2.0.1"}} 6 | -------------------------------------------------------------------------------- /src/re_frame/cofx.cljc: -------------------------------------------------------------------------------- 1 | (ns re-frame.cofx 2 | (:require [re-frame.interceptor :refer [->interceptor]] 3 | [re-frame.registry :as reg] 4 | [lambdaisland.glogi :as log])) 5 | 6 | 7 | ;; -- Registration ------------------------------------------------------------ 8 | 9 | (def kind :cofx) 10 | (assert (re-frame.registry/kinds kind)) 11 | 12 | ;; -- Interceptor ------------------------------------------------------------- 13 | 14 | (defn inject-cofx 15 | "Given an `id`, and an optional, arbitrary `value`, returns an interceptor 16 | whose `:before` adds to the `:coeffects` (map) by calling a pre-registered 17 | 'coeffect handler' identified by the `id`. 18 | 19 | The previous association of a `coeffect handler` with an `id` will have 20 | happened via a call to `re-frame.core/reg-cofx` - generally on program startup. 21 | 22 | Within the created interceptor, this 'looked up' `coeffect handler` will 23 | be called (within the `:before`) with two arguments: 24 | - the current value of `:coeffects` 25 | - optionally, the originally supplied arbitrary `value` 26 | 27 | This `coeffect handler` is expected to modify and return its first, `coeffects` argument. 28 | 29 | Example Of how `inject-cofx` and `reg-cofx` work together 30 | --------------------------------------------------------- 31 | 32 | 1. Early in app startup, you register a `coeffect handler` for `:datetime`: 33 | 34 | (re-frame.core/reg-cofx 35 | :datetime ;; usage (inject-cofx :datetime) 36 | (fn coeffect-handler 37 | [coeffect] 38 | (assoc coeffect :now (js/Date.)))) ;; modify and return first arg 39 | 40 | 2. Later, add an interceptor to an -fx event handler, using `inject-cofx`: 41 | 42 | (re-frame.core/reg-event-fx ;; we are registering an event handler 43 | :event-id 44 | [ ... (inject-cofx :datetime) ... ] ;; <-- create an injecting interceptor 45 | (fn event-handler 46 | [coeffect event] 47 | ... in here can access (:now coeffect) to obtain current datetime ... ))) 48 | 49 | Background 50 | ---------- 51 | 52 | `coeffects` are the input resources required by an event handler 53 | to perform its job. The two most obvious ones are `db` and `event`. 54 | But sometimes an event handler might need other resources. 55 | 56 | Perhaps an event handler needs a random number or a GUID or the current 57 | datetime. Perhaps it needs access to a DataScript database connection. 58 | 59 | If an event handler directly accesses these resources, it stops being 60 | pure and, consequently, it becomes harder to test, etc. So we don't 61 | want that. 62 | 63 | Instead, the interceptor created by this function is a way to 'inject' 64 | 'necessary resources' into the `:coeffects` (map) subsequently given 65 | to the event handler at call time." 66 | ([registry id] 67 | (->interceptor 68 | :id :coeffects 69 | :before (fn coeffects-before 70 | [context] 71 | (if-let [handler (reg/get-handler registry kind id)] 72 | (update context :coeffects handler (:frame context)) 73 | (log/error :missing-cofx-handler {:id id}))))) 74 | ([registry id value] 75 | (->interceptor 76 | :id :coeffects 77 | :before (fn coeffects-before 78 | [context] 79 | (if-let [handler (reg/get-handler registry kind id)] 80 | (update context :coeffects handler value (:frame context)) 81 | (log/error :missing-cofx-handler {:id id})))))) 82 | 83 | 84 | ;; -- Builtin CoEffects Handlers --------------------------------------------- 85 | 86 | (defn register-built-in! 87 | [{:keys [registry]}] 88 | (let [reg-cofx (partial reg/register-handler registry kind)] 89 | (reg-cofx 90 | :db 91 | (fn db-coeffects-handler 92 | [coeffects frame] 93 | (assoc coeffects :db @(:app-db frame)))))) 94 | 95 | ;; Because this interceptor is used so much, we reify it 96 | ;; (def inject-db (inject-cofx :db)) 97 | -------------------------------------------------------------------------------- /src/re_frame/context.clj: -------------------------------------------------------------------------------- 1 | (ns re-frame.context 2 | (:refer-clojure :exclude [bound-fn]) 3 | (:require [cljs.env] 4 | [cljs.analyzer])) 5 | 6 | (defmacro defc 7 | "For definining Reagent components that honor the contextual frame. Like defn 8 | but sets a :context-type metadata on the function, which Reagent will pick up 9 | on, so that the correct React context is set for this component." 10 | [name & fntail] 11 | (let [[doc fntail] (if (string? (first fntail)) 12 | [(first fntail) (rest fntail)] 13 | [nil fntail])] 14 | `(def ~(with-meta name (merge {:doc doc} (:meta &form))) 15 | ^{:context-type frame-context} 16 | (fn ~@fntail)))) 17 | 18 | (defmacro bind-frame [frame & body] 19 | `(binding [~'re-frame.registry/*current-frame* ~frame] 20 | (assert (satisfies? ~'re-frame.frame/IFrame ~frame) "given frame is not of type `re-frame.frame/IFrame`") 21 | ~@body)) 22 | 23 | (defmacro import-with-frame 24 | ([var-sym] 25 | `(import-with-frame ~(symbol (name var-sym)) ~var-sym)) 26 | ([name var-sym] 27 | `(defn ~name 28 | ;; Attempt at propagating the doc string / arglists, for some reason CIDER 29 | ;; is not picking this up though. 30 | ~(select-keys (:meta (cljs.analyzer/resolve-var cljs.env/*compiler* var-sym)) 31 | [:doc :arglists]) 32 | [& args#] 33 | (apply ~var-sym (current-frame) args#)))) 34 | 35 | (defmacro bound-fn [& args] 36 | (let [[name argv & body] (if (symbol? (first args)) 37 | args 38 | (into [nil] args))] 39 | `(let [frame# (~'re-frame.context/current-frame)] 40 | (fn ~@(when name name) ~argv 41 | (binding [~'re-frame.registry/*current-frame* frame#] 42 | ~@body))))) 43 | -------------------------------------------------------------------------------- /src/re_frame/context.cljs: -------------------------------------------------------------------------------- 1 | (ns re-frame.context 2 | (:require ["react" :as react] 3 | [goog.object :as gobj] 4 | [lambdaisland.glogi :as log] 5 | [re-frame.core :as r] 6 | [re-frame.frame :as frame] 7 | [re-frame.registry :as registry] 8 | [re-frame.subs :as subs] 9 | [reagent.core]) 10 | (:require-macros [re-frame.context :refer [defc import-with-frame]])) 11 | 12 | (def frame-context (.createContext react r/default-frame)) 13 | 14 | (defn set-default-frame [frame] 15 | (gobj/set frame-context "_currentValue" frame) 16 | (gobj/set frame-context "_currentValue2" frame)) 17 | 18 | (defn current-context 19 | "Gets the react Context for the current component, to be used in lifecycle 20 | hooks (e.g. render). Assumes that Component.contextType has been set." 21 | [] 22 | (when-let [cmp (reagent.core/current-component)] 23 | ;; When used without setting the right contextType we will get #js {} back 24 | (when (not (object? (.-context cmp))) 25 | (.-context cmp)))) 26 | 27 | (defn current-frame 28 | "Get the current frame provided by the context, falling back to the default 29 | frame. Assumes that Component.contextType = frame-context." 30 | [] 31 | (or registry/*current-frame* 32 | (current-context) 33 | (gobj/get frame-context "_currentValue"))) 34 | 35 | (defn bound-frame [] 36 | (or registry/*current-frame* 37 | (current-context) 38 | (throw (js/Error. "No frame bound")))) 39 | 40 | (defn provide-frame 41 | "Component that acts as a provider for the frame, so to run an isolated version 42 | of your app, use. 43 | 44 | [provide-frame (frame/make-frame) 45 | [app]]" 46 | [frame & children] 47 | (reagent.core/create-element 48 | (.-Provider frame-context) 49 | #js {:value frame 50 | :children (reagent.core/as-element (into [:<>] children))})) 51 | 52 | (defc provide-app-db 53 | "Component that acts as a provider for the app-db, it takes the registry from 54 | the current frame, but uses the given atom for the app-db" 55 | [app-db & children] 56 | `[~provide-frame ~(frame/make-frame {:registry (:registry (current-frame)) 57 | :app-db app-db}) 58 | ~@children]) 59 | 60 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 61 | ;; Complete copy of the top-level re-frame API. If you are using the context 62 | ;; approach then import re-frame.context instead of re-frame.core and things 63 | ;; should generally Just Work™ 64 | 65 | (import-with-frame subscribe re-frame.frame/subscribe) 66 | (import-with-frame dispatch re-frame.frame/dispatch) 67 | (import-with-frame dispatch-sync re-frame.frame/dispatch-sync) 68 | (import-with-frame clear-sub re-frame.frame/clear-sub) 69 | (import-with-frame reg-fx re-frame.frame/reg-fx) 70 | (import-with-frame reg-cofx re-frame.frame/reg-cofx) 71 | (import-with-frame inject-cofx re-frame.frame/inject-cofx) 72 | (import-with-frame clear-cofx re-frame.frame/clear-cofx) 73 | (import-with-frame reg-event-db re-frame.frame/reg-event-db) 74 | (import-with-frame reg-event-fx re-frame.frame/reg-event-fx) 75 | (import-with-frame reg-event-ctx re-frame.frame/reg-event-ctx) 76 | (import-with-frame clear-event re-frame.frame/clear-event) 77 | 78 | ;; A few special cases which we can't import directly 79 | 80 | (defn reg-sub-raw [query-id handler-fn] 81 | (frame/reg-sub-raw 82 | (current-frame) 83 | query-id 84 | (fn [frame query-v] 85 | (handler-fn (:app-db frame) query-v)))) 86 | 87 | ;; some slight weirdness here because protocols don't support variadic functions 88 | (defn reg-sub [query-id & args] 89 | (frame/reg-sub (current-frame) query-id args)) 90 | 91 | (defn clear-subscriptions-cache! [& args] 92 | (apply subs/-clear (:subs-cache (current-frame)) args)) 93 | 94 | 95 | (defn context-fns 96 | "Returns subscribe/dispatch/dispatch-sync functions that are bound to the current frame. Use like this 97 | 98 | (defc my-component [] 99 | (reagent/with-let [{:keys [subscribe dispatch]} (re-frame/context-fns)] 100 | ,,, 101 | )) " 102 | ([] (context-fns (current-frame))) 103 | ([frame] 104 | {:subscribe (partial re-frame.frame/subscribe frame) 105 | :dispatch (partial re-frame.frame/dispatch frame) 106 | :dispatch-sync (partial re-frame.frame/dispatch-sync frame)})) 107 | 108 | (defn bind-fn [f] 109 | (let [frame (current-frame)] 110 | (fn [& args] 111 | (binding [registry/*current-frame* frame] 112 | (apply f args))))) 113 | -------------------------------------------------------------------------------- /src/re_frame/core.cljc: -------------------------------------------------------------------------------- 1 | (ns re-frame.core 2 | (:require [re-frame.events :as events] 3 | [re-frame.subs :as subs] 4 | [re-frame.frame :as frame] 5 | [re-frame.interop :as interop] 6 | [re-frame.fx :as fx] 7 | [re-frame.cofx :as cofx] 8 | [re-frame.router :as router] 9 | [re-frame.loggers :as loggers] 10 | [re-frame.registry :as reg] 11 | [re-frame.interceptor :as interceptor] 12 | [re-frame.std-interceptors :as std-interceptors])) 13 | 14 | ;; -- API --------------------------------------------------------------------- 15 | ;; 16 | ;; This namespace represents the re-frame API 17 | ;; 18 | ;; Below, you'll see we've used this technique: 19 | ;; (def api-name-for-fn deeper.namespace/where-the-defn-is) 20 | ;; 21 | ;; So, we promote a `defn` in a deeper namespace "up" to the API 22 | ;; via a `def` in this namespace. 23 | ;; 24 | ;; Turns out, this approach makes it hard: 25 | ;; - to auto-generate API docs 26 | ;; - for IDEs to provide code completion on functions in the API 27 | ;; 28 | ;; Which is annoying. But there are pros and cons and we haven't 29 | ;; yet revisited the decision. To compensate, we've added more nudity 30 | ;; to the docs. 31 | ;; 32 | 33 | ;; -- interceptor related 34 | ;; useful if you are writing your own interceptors 35 | (def ->interceptor interceptor/->interceptor) 36 | (def enqueue interceptor/enqueue) 37 | (def get-coeffect interceptor/get-coeffect) 38 | (def get-effect interceptor/get-effect) 39 | (def assoc-effect interceptor/assoc-effect) 40 | (def assoc-coeffect interceptor/assoc-coeffect) 41 | 42 | 43 | ;; -- standard interceptors 44 | (def debug std-interceptors/debug) 45 | (def path std-interceptors/path) 46 | (def enrich std-interceptors/enrich) 47 | (def trim-v std-interceptors/trim-v) 48 | (def after std-interceptors/after) 49 | (def on-changes std-interceptors/on-changes) 50 | 51 | ;; XXX move API functions up to this core level - to enable code completion and docs 52 | ;; XXX on figwheel reload, is there a way to not get the re-registration messages. 53 | 54 | ;; Export API wrapping `default-frame` singleton --- 55 | 56 | (def default-frame (frame/make-frame)) 57 | 58 | (fx/register-built-in! default-frame) 59 | (cofx/register-built-in! default-frame) 60 | 61 | (def dispatch (partial frame/dispatch default-frame)) 62 | (def dispatch-sync (partial frame/dispatch-sync default-frame)) 63 | 64 | (defn reg-sub-raw [query-id handler-fn] 65 | (frame/reg-sub-raw 66 | default-frame 67 | query-id 68 | (fn [frame query-v] 69 | (handler-fn (:app-db frame) query-v)))) 70 | 71 | ;; some slight weirdness here because protocols don't support variadic functions 72 | (defn reg-sub [query-id & args] 73 | (frame/reg-sub default-frame query-id args)) 74 | 75 | (def subscribe (partial frame/subscribe default-frame)) 76 | (def clear-sub (partial frame/clear-sub default-frame)) 77 | (def clear-subscriptions-cache! (partial subs/-clear (:subs-cache default-frame))) 78 | 79 | (def reg-fx (partial frame/reg-fx default-frame)) 80 | (def clear-fx (partial frame/clear-fx default-frame)) 81 | 82 | (def reg-cofx (partial frame/reg-cofx default-frame)) 83 | (def inject-cofx (partial frame/inject-cofx default-frame)) 84 | (def clear-cofx (partial frame/clear-cofx default-frame)) 85 | 86 | (def reg-event-db (partial frame/reg-event-db default-frame)) 87 | (def reg-event-fx (partial frame/reg-event-fx default-frame)) 88 | (def reg-event-ctx (partial frame/reg-event-ctx default-frame)) 89 | (def clear-event (partial frame/clear-event default-frame)) 90 | 91 | ;; -- logging ---------------------------------------------------------------- 92 | ;; Internally, re-frame uses the logging functions: warn, log, error, group and groupEnd 93 | ;; By default, these functions map directly to the js/console implementations, 94 | ;; but you can override with your own fns (set or subset). 95 | ;; Example Usage: 96 | ;; (defn my-fn [& args] (post-it-somewhere (apply str args))) ;; here is my alternative 97 | ;; (re-frame.core/set-loggers! {:warn my-fn :log my-fn}) ;; override the defaults with mine 98 | (def set-loggers! loggers/set-loggers!) 99 | 100 | ;; If you are writing an extension to re-frame, like perhaps 101 | ;; an effects handler, you may want to use re-frame logging. 102 | ;; 103 | ;; usage: (console :error "Oh, dear God, it happened: " a-var " and " another) 104 | ;; (console :warn "Possible breach of containment wall at: " dt) 105 | (def console loggers/console) 106 | 107 | 108 | ;; -- unit testing ------------------------------------------------------------ 109 | 110 | (defn make-restore-fn 111 | "Checkpoints the state of re-frame and returns a function which, when 112 | later called, will restore re-frame to that checkpointed state. 113 | 114 | Checkpoint includes app-db, all registered handlers and all subscriptions." 115 | [] 116 | (frame/make-restore-fn default-frame)) 117 | 118 | (defn purge-event-queue 119 | "Remove all events queued for processing" 120 | [] 121 | (router/purge (:event-queue default-frame))) 122 | 123 | ;; -- Event Processing Callbacks --------------------------------------------- 124 | 125 | (defn add-post-event-callback 126 | "Registers a function `f` to be called after each event is processed 127 | `f` will be called with two arguments: 128 | - `event`: a vector. The event just processed. 129 | - `queue`: a PersistentQueue, possibly empty, of events yet to be processed. 130 | 131 | This is useful in advanced cases like: 132 | - you are implementing a complex bootstrap pipeline 133 | - you want to create your own handling infrastructure, with perhaps multiple 134 | handlers for the one event, etc. Hook in here. 135 | - libraries providing 'isomorphic javascript' rendering on Nodejs or Nashorn. 136 | 137 | 'id' is typically a keyword. Supplied at \"add time\" so it can subsequently 138 | be used at \"remove time\" to get rid of the right callback." 139 | ([f] 140 | (add-post-event-callback f f)) ;; use f as its own identifier 141 | ([id f] 142 | (router/add-post-event-callback (:event-queue default-frame) id f))) 143 | 144 | 145 | (defn remove-post-event-callback 146 | [id] 147 | (router/remove-post-event-callback (:event-queue default-frame) id)) 148 | -------------------------------------------------------------------------------- /src/re_frame/events.cljc: -------------------------------------------------------------------------------- 1 | (ns re-frame.events 2 | (:require [re-frame.utils :refer [first-in-vector]] 3 | [re-frame.interop :refer [empty-queue debug-enabled?]] 4 | [re-frame.registry :as reg] 5 | [re-frame.loggers :refer [console]] 6 | [re-frame.interceptor :as interceptor] 7 | [re-frame.trace :as trace :include-macros true])) 8 | 9 | 10 | (def kind :event) 11 | (assert (re-frame.registry/kinds kind)) 12 | 13 | (defn- flatten-and-remove-nils 14 | "`interceptors` might have nested collections, and contain nil elements. 15 | return a flat collection, with all nils removed. 16 | This function is 9/10 about giving good error messages." 17 | [id interceptors] 18 | (let [make-chain #(->> % flatten (remove nil?))] 19 | (if-not debug-enabled? 20 | (make-chain interceptors) 21 | (do ;; do a whole lot of development time checks 22 | (when-not (coll? interceptors) 23 | (console :error "re-frame: when registering" id ", expected a collection of interceptors, got:" interceptors)) 24 | (let [chain (make-chain interceptors)] 25 | (when (empty? chain) 26 | (console :error "re-frame: when registering" id ", given an empty interceptor chain")) 27 | (when-let [not-i (first (remove interceptor/interceptor? chain))] 28 | (if (fn? not-i) 29 | (console :error "re-frame: when registering" id ", got a function instead of an interceptor. Did you provide old style middleware by mistake? Got:" not-i) 30 | (console :error "re-frame: when registering" id ", expected interceptors, but got:" not-i))) 31 | chain))))) 32 | 33 | 34 | (defn register 35 | "Associate the given event `id` with the given collection of `interceptors`. 36 | 37 | `interceptors` may contain nested collections and there may be nils 38 | at any level,so process this structure into a simple, nil-less vector 39 | before registration. 40 | 41 | Typically, an `event handler` will be at the end of the chain (wrapped 42 | in an interceptor)." 43 | [registry id interceptors] 44 | (reg/register-handler registry 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 53 | "Given an event vector `event-v`, look up the associated interceptor chain, and execute it." 54 | [{:keys [registry app-db] :as frame} event-v] 55 | (let [event-id (first-in-vector event-v)] 56 | (when-let [interceptors (reg/get-handler registry kind event-id true)] 57 | (if *handling* 58 | (console :error "re-frame: while handling" *handling* ", dispatch-sync was called for" event-v ". You can't call dispatch-sync within an event handler.") 59 | (binding [*handling* event-v] 60 | (trace/with-trace {:operation event-id 61 | :op-type kind 62 | :tags {:event event-v}} 63 | (trace/merge-trace! {:tags {:app-db-before @app-db}}) 64 | (interceptor/execute frame event-v interceptors) 65 | (trace/merge-trace! {:tags {:app-db-after @app-db}}))))))) 66 | -------------------------------------------------------------------------------- /src/re_frame/frame.cljc: -------------------------------------------------------------------------------- 1 | (ns re-frame.frame 2 | "An instance of freerange's state. 3 | 4 | A frame combines all of freerange's state in a single object, so it is 5 | possible to have multiple, isolated instances. " 6 | (:require [re-frame.cofx :as cofx] 7 | [re-frame.events :as events] 8 | [re-frame.fx :as fx] 9 | [re-frame.interop :as interop] 10 | [re-frame.registry :as reg] 11 | [re-frame.router :as router] 12 | [re-frame.std-interceptors :as stdi] 13 | [re-frame.subs :as subs])) 14 | 15 | (defprotocol IFrame 16 | ;; dispatch ---- 17 | (dispatch [this event-v]) 18 | (dispatch-sync [this event-v]) 19 | 20 | ;; subs ---- 21 | (reg-sub-raw [this query-id handler-fn]) 22 | (reg-sub [this query-id args]) 23 | (subscribe [this query-v]) 24 | (clear-sub [this query-id]) 25 | (clear-subscriptions-cache [this]) 26 | 27 | ;; fx ---- 28 | (reg-fx [this fx-id handler-fn]) 29 | (clear-fx [this fx-id]) 30 | 31 | ;; cofx ---- 32 | (reg-cofx [this cofx-id handler-fn]) 33 | (inject-cofx 34 | [this cofx-id] 35 | [this cofx-id value]) 36 | (clear-cofx [this cofx-id]) 37 | 38 | ;; events ---- 39 | (clear-event [this event-id]) 40 | (reg-event-db 41 | [this id db-handler] 42 | [this id interceptors db-handler]) 43 | (reg-event-fx 44 | [this id fx-handler] 45 | [this id interceptors fx-handler]) 46 | (reg-event-ctx 47 | [this id handler] 48 | [this id interceptors handler])) 49 | 50 | ;; connect all the pieces of state ---- 51 | (defrecord Frame [registry event-queue app-db subs-cache default-interceptors] 52 | IFrame 53 | ;; dispatch ---- 54 | (dispatch [this event-v] 55 | (router/dispatch event-queue event-v)) 56 | (dispatch-sync [this event-v] 57 | (router/dispatch-sync this event-v)) 58 | 59 | ;; subs ---- 60 | (reg-sub-raw [this query-id handler-fn] 61 | (reg/register-handler registry subs/kind query-id handler-fn)) 62 | (reg-sub [this query-id args] 63 | (apply subs/reg-sub this query-id args)) 64 | (subscribe [this query-v] 65 | (subs/subscribe this query-v)) 66 | (clear-sub [this query-id] 67 | (reg/clear-handlers registry subs/kind query-id)) 68 | (clear-subscriptions-cache [this] 69 | (subs/clear-subscription-cache! subs-cache)) 70 | 71 | ;; fx ---- 72 | (reg-fx [this fx-id handler-fn] 73 | (reg/register-handler registry fx/kind fx-id handler-fn)) 74 | (clear-fx [this fx-id] 75 | (reg/clear-handlers registry fx/kind fx-id)) 76 | 77 | ;; cofx ---- 78 | (reg-cofx [this cofx-id handler-fn] 79 | (reg/register-handler registry cofx/kind cofx-id handler-fn)) 80 | (inject-cofx [this cofx-id] 81 | (cofx/inject-cofx registry cofx-id)) 82 | (inject-cofx [this cofx-id value] 83 | (cofx/inject-cofx registry cofx-id value)) 84 | (clear-cofx [this cofx-id] 85 | (reg/clear-handlers registry cofx/kind cofx-id)) 86 | 87 | ;; events ---- 88 | (clear-event [this id] 89 | (reg/clear-handlers registry events/kind id)) 90 | 91 | (reg-event-db [this id db-handler] 92 | (reg-event-db this id nil db-handler)) 93 | (reg-event-db [this id interceptors db-handler] 94 | (events/register 95 | registry 96 | id 97 | [default-interceptors interceptors (stdi/db-handler->interceptor db-handler)])) 98 | (reg-event-fx [this id fx-handler] 99 | (reg-event-fx this id nil fx-handler)) 100 | (reg-event-fx [this id interceptors fx-handler] 101 | (events/register 102 | registry 103 | id 104 | [default-interceptors interceptors (stdi/fx-handler->interceptor fx-handler)])) 105 | (reg-event-ctx [this id handler] 106 | (reg-event-ctx this id nil handler)) 107 | (reg-event-ctx [this id interceptors handler] 108 | (events/register 109 | registry 110 | id 111 | [default-interceptors interceptors (stdi/ctx-handler->interceptor handler)]))) 112 | 113 | (def frame-id (atom 0)) 114 | 115 | (defn make-frame 116 | "Creates a new frame, which bundles the registry (subscriptions, event-handlers, 117 | fx, cofx), app-db, subscription cache, default interceptors, and event queue. 118 | 119 | :registry, :app-db, and :interceptors can be provided through an options map." 120 | [& [{:keys [registry app-db interceptors] :as extra-keys}]] 121 | (let [registry (or registry (reg/make-registry)) 122 | app-db (or app-db (interop/ratom {})) 123 | default-interceptors [(cofx/inject-cofx registry :db) 124 | (fx/do-fx registry)] 125 | frame (map->Frame 126 | (merge {:frame-id (swap! frame-id inc) 127 | :registry registry 128 | :app-db app-db 129 | :subs-cache (subs/->SubscriptionCache (atom {})) 130 | :default-interceptors (if interceptors 131 | (if (:replace (meta interceptors)) 132 | interceptors 133 | (into default-interceptors interceptors)) 134 | default-interceptors) 135 | :event-queue (router/->EventQueue :idle interop/empty-queue {} nil)} 136 | (dissoc extra-keys :registry :app-db :interceptors)))] 137 | ;; When events / fx fire, they get their frame from the event-queue 138 | (set! (.-frame (:event-queue frame)) frame) 139 | frame)) 140 | 141 | (defn make-restore-fn 142 | "Checkpoints the state of re-frame and returns a function which, when 143 | later called, will restore re-frame to that checkpointed state. 144 | 145 | Checkpoint includes app-db, all registered handlers and all subscriptions." 146 | ([frame] 147 | (let [handlers (-> frame :registry :kind->id->handler deref) 148 | app-db (-> frame :app-db deref) 149 | subs-cache (-> frame :subs-cache deref)] 150 | (fn [] 151 | ;; call `dispose!` on all current subscriptions which 152 | ;; didn't originally exist. 153 | (let [original-subs (-> subs-cache vals set) 154 | current-subs (-> frame :subs-cache deref vals)] 155 | (doseq [sub current-subs 156 | :when (not (contains? original-subs sub))] 157 | (interop/dispose! sub))) 158 | 159 | ;; Reset the atoms 160 | ;; We don't need to reset subs-cache, as disposing of the subs 161 | ;; removes them from the cache anyway 162 | (reset! (-> frame :registry :kind->id->handler) handlers) 163 | (reset! (-> frame :app-db) app-db) 164 | nil)))) 165 | -------------------------------------------------------------------------------- /src/re_frame/fx.cljc: -------------------------------------------------------------------------------- 1 | (ns re-frame.fx 2 | (:require [re-frame.router :as router] 3 | [re-frame.interceptor :refer [->interceptor]] 4 | [re-frame.interop :refer [set-timeout!]] 5 | [re-frame.events :as events] 6 | [re-frame.registry :as reg] 7 | [re-frame.loggers :refer [console]] 8 | [re-frame.trace :as trace :include-macros true] 9 | [lambdaisland.glogi :as log])) 10 | 11 | ;; -- Registration ------------------------------------------------------------ 12 | 13 | (def kind :fx) 14 | (assert (re-frame.registry/kinds kind)) 15 | 16 | ;; -- Interceptor ------------------------------------------------------------- 17 | 18 | (defn register-built-in! 19 | [{:keys [registry]}] 20 | (let [reg-fx (partial reg/register-handler registry kind)] 21 | 22 | ;; :dispatch-later 23 | ;; 24 | ;; `dispatch` one or more events after given delays. Expects a collection 25 | ;; of maps with two keys: :`ms` and `:dispatch` 26 | ;; 27 | ;; usage: 28 | ;; 29 | ;; {:dispatch-later [{:ms 200 :dispatch [:event-id "param"]} ;; in 200ms do this: (dispatch [:event-id "param"]) 30 | ;; {:ms 100 :dispatch [:also :this :in :100ms]}]} 31 | ;; 32 | ;; Note: nil entries in the collection are ignored which means events can be added 33 | ;; conditionally: 34 | ;; {:dispatch-later [ (when (> 3 5) {:ms 200 :dispatch [:conditioned-out]}) 35 | ;; {:ms 100 :dispatch [:another-one]}]} 36 | ;; 37 | (reg-fx 38 | :dispatch-later 39 | (fn [value {:keys [event-queue]}] 40 | (doseq [{:keys [ms dispatch] :as effect} (remove nil? value)] 41 | (if (or (empty? dispatch) (not (number? ms))) 42 | (console :error "re-frame: ignoring bad :dispatch-later value:" effect) 43 | (set-timeout! #(router/dispatch event-queue dispatch) ms))))) 44 | 45 | 46 | ;; :dispatch 47 | ;; 48 | ;; `dispatch` one event. Expects a single vector. 49 | ;; 50 | ;; usage: 51 | ;; {:dispatch [:event-id "param"] } 52 | 53 | (reg-fx 54 | :dispatch 55 | (fn [value {:keys [event-queue]}] 56 | (if-not (vector? value) 57 | (console :error "re-frame: ignoring bad :dispatch value. Expected a vector, but got:" value) 58 | (router/dispatch event-queue value)))) 59 | 60 | 61 | ;; :dispatch-n 62 | ;; 63 | ;; `dispatch` more than one event. Expects a list or vector of events. Something for which 64 | ;; sequential? returns true. 65 | ;; 66 | ;; usage: 67 | ;; {:dispatch-n (list [:do :all] [:three :of] [:these])} 68 | ;; 69 | ;; Note: nil events are ignored which means events can be added 70 | ;; conditionally: 71 | ;; {:dispatch-n (list (when (> 3 5) [:conditioned-out]) 72 | ;; [:another-one])} 73 | ;; 74 | (reg-fx 75 | :dispatch-n 76 | (fn [value {:keys [event-queue]}] 77 | (if-not (sequential? value) 78 | (console :error "re-frame: ignoring bad :dispatch-n value. Expected a collection, but got:" value) 79 | (doseq [event (remove nil? value)] (router/dispatch event-queue event))))) 80 | 81 | 82 | ;; :deregister-event-handler 83 | ;; 84 | ;; removes a previously registered event handler. Expects either a single id ( 85 | ;; typically a namespaced keyword), or a seq of ids. 86 | ;; 87 | ;; usage: 88 | ;; {:deregister-event-handler :my-id)} 89 | ;; or: 90 | ;; {:deregister-event-handler [:one-id :another-id]} 91 | ;; 92 | (reg-fx 93 | :deregister-event-handler 94 | (fn [value {:keys [registry]}] 95 | (let [clear-event (partial reg/clear-handlers registry events/kind)] 96 | (if (sequential? value) 97 | (doseq [event value] (clear-event event)) 98 | (clear-event value))))) 99 | 100 | 101 | ;; :db 102 | ;; 103 | ;; reset! app-db with a new value. `value` is expected to be a map. 104 | ;; 105 | ;; usage: 106 | ;; {:db {:key1 value1 key2 value2}} 107 | ;; 108 | (reg-fx 109 | :db 110 | (fn [value {:keys [app-db]}] 111 | (when-not (identical? @app-db value) 112 | (reset! app-db value)))))) 113 | 114 | ;; -- Builtin Effect Handlers ------------------------------------------------ 115 | 116 | (defn do-fx 117 | "An interceptor whose `:after` actions the contents of `:effects`. As a result 118 | this interceptor is Domino 3. 119 | 120 | This interceptor is silently added (by reg-event-db etc) to the front of 121 | interceptor chains for all events. 122 | 123 | For each key in `:effects` (a map), it calls the registered `effects handler` 124 | (see `reg-fx` for registration of effect handlers). 125 | 126 | So, if `:effects` was: 127 | {:dispatch [:hello 42] 128 | :db {...} 129 | :undo \"set flag\"} 130 | 131 | it will call the registered effect handlers for each of the map's keys: 132 | `:dispatch`, `:undo` and `:db`. When calling each handler, provides the map 133 | value for that key - so in the example above the effect handler for :dispatch 134 | will be given one arg `[:hello 42]`. 135 | 136 | You cannot rely on the ordering in which effects are executed." 137 | [registry] 138 | (->interceptor 139 | :id :do-fx 140 | :after (fn do-fx-after 141 | [context] 142 | {:pre [(:frame context)]} 143 | (trace/with-trace 144 | {:op-type :event/do-fx} 145 | (doseq [[effect-key effect-value] (:effects context)] 146 | (if-let [effect-fn (reg/get-handler registry kind effect-key)] 147 | (effect-fn effect-value (:frame context)) 148 | (log/error :missing-fx-handler {:effect-key effect-key}))))))) 149 | -------------------------------------------------------------------------------- /src/re_frame/interop.clj: -------------------------------------------------------------------------------- 1 | (ns re-frame.interop 2 | (:import [java.util.concurrent Executor Executors])) 3 | 4 | ;; The purpose of this file is to provide JVM-runnable implementations of the 5 | ;; CLJS equivalents in interop.cljs. 6 | ;; 7 | ;; These implementations are to enable you to bring up a re-frame app on the JVM 8 | ;; in order to run tests, or to develop at a JVM REPL instead of a CLJS one. 9 | ;; 10 | ;; Please note, though, that the purpose here *isn't* to fully replicate all of 11 | ;; re-frame's behaviour in a real CLJS environment. We don't have Reagent or 12 | ;; React on the JVM, and we don't try to mimic the stateful lifecycles that they 13 | ;; embody. 14 | ;; 15 | ;; In particular, if you're performing side effects in any code that's triggered 16 | ;; by a change to a Ratom's value, and not via a call to `dispatch`, then you're 17 | ;; going to have a hard time getting any accurate tests with this code. 18 | ;; However, if your subscriptions and Reagent render functions are pure, and 19 | ;; your side-effects are all managed by effect handlers, then hopefully this will 20 | ;; allow you to write some useful tests that can run on the JVM. 21 | 22 | 23 | (defonce ^:private executor (Executors/newSingleThreadExecutor)) 24 | 25 | (defonce ^:private on-dispose-callbacks (atom {})) 26 | 27 | (defn next-tick [f] 28 | (let [bound-f (bound-fn [& args] (apply f args))] 29 | (.execute ^Executor executor bound-f)) 30 | nil) 31 | 32 | (def empty-queue clojure.lang.PersistentQueue/EMPTY) 33 | 34 | (def after-render next-tick) 35 | 36 | (def debug-enabled? true) 37 | 38 | (defn ratom [x] 39 | (atom x)) 40 | 41 | (defn ratom? [x] 42 | (instance? clojure.lang.IAtom x)) 43 | 44 | (defn deref? [x] 45 | (instance? clojure.lang.IDeref x)) 46 | 47 | (defn make-reaction 48 | "On JVM Clojure, return a `deref`-able thing which invokes the given function 49 | on every `deref`. That is, `make-reaction` here provides precisely none of the 50 | benefits of `reagent.ratom/make-reaction` (which only invokes its function if 51 | the reactions that the function derefs have changed value). But so long as `f` 52 | only depends on other reactions (which also behave themselves), the only 53 | difference is one of efficiency. That is, your tests should see no difference 54 | other than that they do redundant work." 55 | [f] 56 | (reify clojure.lang.IDeref 57 | (deref [_] (f)))) 58 | 59 | (defn add-on-dispose! 60 | "On JVM Clojure, use an atom to register `f` to be invoked when `dispose!` is 61 | invoked with `a-ratom`." 62 | [a-ratom f] 63 | (swap! on-dispose-callbacks update a-ratom (fnil conj []) f) 64 | nil) 65 | 66 | (defn dispose! 67 | "On JVM Clojure, invoke all callbacks registered with `add-on-dispose!` for 68 | `a-ratom`." 69 | [a-ratom] 70 | ;; Try to replicate reagent's behavior, releasing resources first then 71 | ;; invoking callbacks 72 | (let [callbacks (get @on-dispose-callbacks a-ratom)] 73 | (swap! on-dispose-callbacks dissoc a-ratom) 74 | (doseq [f callbacks] (f)))) 75 | 76 | (defn set-timeout! 77 | "Note that we ignore the `ms` value and just invoke the function, because 78 | there isn't often much point firing a timed event in a test." 79 | [f ms] 80 | (next-tick f)) 81 | 82 | (defn now [] 83 | ;; currentTimeMillis may count backwards in some scenarios, but as this is used for tracing 84 | ;; it is preferable to the slower but more accurate System.nanoTime. 85 | (System/currentTimeMillis)) 86 | 87 | (defn reagent-id 88 | "Doesn't make sense in a Clojure context currently." 89 | [reactive-val] 90 | "rx-clj") 91 | -------------------------------------------------------------------------------- /src/re_frame/interop.cljs: -------------------------------------------------------------------------------- 1 | (ns re-frame.interop 2 | (:require [reagent.core] 3 | [reagent.impl.batching] 4 | [reagent.ratom] 5 | [react :as react] 6 | [react-dom :as react-dom])) 7 | 8 | (def next-tick reagent.impl.batching/do-before-flush) 9 | 10 | (def empty-queue #queue []) 11 | 12 | (def after-render reagent.core/after-render) 13 | 14 | ;; Make sure the Google Closure compiler sees this as a boolean constant, 15 | ;; otherwise Dead Code Elimination won't happen in `:advanced` builds. 16 | ;; Type hints have been liberally sprinkled. 17 | ;; https://developers.google.com/closure/compiler/docs/js-for-compiler 18 | (def ^boolean debug-enabled? "@define {boolean}" ^boolean goog/DEBUG) 19 | 20 | (defn ratom [x] 21 | (reagent.core/atom x)) 22 | 23 | (defn ratom? [x] 24 | (satisfies? reagent.ratom/IReactiveAtom ^clj x)) 25 | 26 | (defn deref? [x] 27 | (satisfies? IDeref x)) 28 | 29 | 30 | (defn make-reaction [f] 31 | (reagent.ratom/make-reaction f)) 32 | 33 | (defn add-on-dispose! [a-ratom f] 34 | (reagent.ratom/add-on-dispose! a-ratom f)) 35 | 36 | (defn dispose! [a-ratom] 37 | (reagent.ratom/dispose! a-ratom)) 38 | 39 | (defn set-timeout! [f ms] 40 | (js/setTimeout f ms)) 41 | 42 | (defn now [] 43 | (if (and 44 | (exists? js/performance) 45 | (exists? js/performance.now)) 46 | (js/performance.now) 47 | (js/Date.now))) 48 | 49 | (defn reagent-id 50 | "Produces an id for reactive Reagent values 51 | e.g. reactions, ratoms, cursors." 52 | [reactive-val] 53 | (when (implements? reagent.ratom/IReactiveAtom ^clj reactive-val) 54 | (str (condp instance? reactive-val 55 | reagent.ratom/RAtom "ra" 56 | reagent.ratom/RCursor "rc" 57 | reagent.ratom/Reaction "rx" 58 | reagent.ratom/Track "tr" 59 | "other") 60 | (hash reactive-val)))) 61 | 62 | ;; Make reagent benefit from batched updates 63 | 64 | (def ^:dynamic *in-batch?* false) 65 | 66 | (defn batch-updates [f] 67 | (react-dom/unstable_batchedUpdates 68 | (fn [] 69 | (binding [*in-batch?* true] 70 | (f))))) 71 | 72 | (let [flush-queues (.bind (.-flush-queues reagent.impl.batching/render-queue) 73 | reagent.impl.batching/render-queue)] 74 | (set! (.-flush-queues reagent.impl.batching/render-queue) 75 | #(batch-updates flush-queues))) 76 | 77 | (let [queue-render reagent.impl.batching/queue-render] 78 | (set! reagent.impl.batching/queue-render (fn [^clj c] 79 | (if *in-batch?* 80 | (.forceUpdate c) 81 | (queue-render c))))) 82 | -------------------------------------------------------------------------------- /src/re_frame/loggers.cljc: -------------------------------------------------------------------------------- 1 | (ns re-frame.loggers 2 | (:require [clojure.set :refer [difference]] 3 | #?@(:clj [[clojure.string :as str] 4 | [clojure.tools.logging :as log]]))) 5 | 6 | #?(:clj (defn log [level & args] 7 | (log/log level (if (= 1 (count args)) 8 | (first args) 9 | (str/join " " args))))) 10 | 11 | 12 | ;; XXX should loggers be put in the registrar ?? 13 | (def ^:private loggers 14 | "Holds the current set of logging functions. 15 | By default, re-frame uses the functions provided by js/console. 16 | Use `set-loggers!` to change these defaults 17 | " 18 | (atom #?(:cljs {:log (js/console.log.bind js/console) 19 | :warn (js/console.warn.bind js/console) 20 | :error (js/console.error.bind js/console) 21 | :debug (js/console.debug.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 | :debug (partial log :debug) 33 | :group (partial log :info) 34 | :groupEnd #()}))) 35 | 36 | (defn console 37 | [level & args] 38 | (assert (contains? @loggers level) (str "re-frame: log called with unknown level: " level)) 39 | (apply (level @loggers) args)) 40 | 41 | 42 | (defn set-loggers! 43 | "Change the set (or a subset) of logging functions used by re-frame. 44 | `new-loggers` should be a map with the same keys as `loggers` (above)" 45 | [new-loggers] 46 | (assert (empty? (difference (set (keys new-loggers)) (-> @loggers keys set))) "Unknown keys in new-loggers") 47 | (swap! loggers merge new-loggers)) 48 | 49 | (defn get-loggers 50 | "Get the current logging functions used by re-frame." 51 | [] 52 | @loggers) 53 | -------------------------------------------------------------------------------- /src/re_frame/registry.cljc: -------------------------------------------------------------------------------- 1 | (ns re-frame.registry 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 | [lambdaisland.glogi :as log])) 7 | 8 | (def ^:dynamic *current-frame*) 9 | 10 | ;; kinds of handlers 11 | (def kinds #{:event :fx :cofx :sub}) 12 | 13 | (defprotocol IRegistry 14 | (get-handler 15 | [this kind] 16 | [this kind id] 17 | [this kind id required?]) 18 | 19 | (register-handler 20 | [this kind id handler-fn]) 21 | 22 | (clear-handlers 23 | [this] 24 | [this kind] 25 | [this kind id])) 26 | 27 | (defrecord Registry [kinds kind->id->handler] 28 | IRegistry 29 | (get-handler [this kind] 30 | (get @kind->id->handler kind)) 31 | (get-handler [this kind id] 32 | (get-in @kind->id->handler [kind id])) 33 | (get-handler [this kind id required?] 34 | (let [handler (get-handler this kind id)] 35 | (when debug-enabled? ;; This is in a separate when so Closure DCE can run 36 | (when (and required? (nil? handler)) ;; Otherwise you'd need to type hint the and with a ^boolean for DCE. 37 | (log/error :missing-handler {:kind kind :id id}))) 38 | handler)) 39 | 40 | (register-handler [this kind id handler-fn] 41 | (when debug-enabled? ;; This is in a separate when so Closure DCE can run 42 | (if (get-handler this kind id false) 43 | (log/trace :overwriting-handler {:kind kind :id id}) 44 | (log/trace :registered-handler {:kind kind :id id}))) ;; allow it, but warn. Happens on figwheel reloads. 45 | (swap! kind->id->handler assoc-in [kind id] handler-fn) 46 | handler-fn) ;; note: returns the just registered handler 47 | 48 | (clear-handlers [this] ;; clear all kinds 49 | (reset! kind->id->handler {})) 50 | (clear-handlers [this kind] ;; clear all handlers for this kind 51 | (assert (kinds kind)) 52 | (swap! kind->id->handler dissoc kind)) 53 | (clear-handlers [this kind id] ;; clear a single handler for a kind 54 | (assert (kinds kind)) 55 | (if (get-handler this kind id) 56 | (swap! kind->id->handler update-in [kind] dissoc id) 57 | (log/warn :msg (str "re-frame: can't clear " kind " handler for " id ". Handler not found."))))) 58 | 59 | (defn make-registry [] 60 | ;; This atom contains a register of all handlers. 61 | ;; Contains a map keyed first by `kind` (of handler), and then `id`. 62 | ;; Leaf nodes are handlers. 63 | (->Registry kinds (atom {}))) 64 | -------------------------------------------------------------------------------- /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 | [re-frame.loggers :refer [console]] 8 | #?(:clj [net.cgrand.macrovich :as macros]) 9 | #?(:cljs [goog.functions]))) 10 | 11 | (def id (atom 0)) 12 | (def ^:dynamic *current-trace* nil) 13 | 14 | (defn reset-tracing! [] 15 | (reset! id 0)) 16 | 17 | #?(:cljs (goog-define trace-enabled? false) 18 | :clj (def ^boolean trace-enabled? false)) 19 | 20 | (defn ^boolean is-trace-enabled? 21 | "See https://groups.google.com/d/msg/clojurescript/jk43kmYiMhA/IHglVr_TPdgJ for more details" 22 | ;; We can remove this extra step of type hinting indirection once our minimum CLJS version includes 23 | ;; https://dev.clojure.org/jira/browse/CLJS-1439 24 | ;; r1.10.63 is the first version with this: 25 | ;; https://github.com/clojure/clojurescript/commit/9ec796d791b1b2bd613af2f62cdecfd25caa6482 26 | [] 27 | trace-enabled?) 28 | 29 | (def trace-cbs (atom {})) 30 | (defonce traces (atom [])) 31 | (defonce next-delivery (atom 0)) 32 | 33 | (defn register-trace-cb 34 | "Registers a tracing callback function which will receive a collection of one or more traces. 35 | Will replace an existing callback function if it shares the same key." 36 | [key f] 37 | (if trace-enabled? 38 | (swap! trace-cbs assoc key f) 39 | (console :warn "Tracing is not enabled. Please set {\"re_frame.trace.trace_enabled_QMARK_\" true} in :closure-defines. See: https://github.com/day8/re-frame-10x#installation."))) 40 | 41 | (defn remove-trace-cb [key] 42 | (swap! trace-cbs dissoc key) 43 | nil) 44 | 45 | (defn next-id [] (swap! id inc)) 46 | 47 | (defn start-trace [{:keys [operation op-type tags child-of]}] 48 | {:id (next-id) 49 | :operation operation 50 | :op-type op-type 51 | :tags tags 52 | :child-of (or child-of (:id *current-trace*)) 53 | :start (interop/now)}) 54 | 55 | ;; On debouncing 56 | ;; 57 | ;; We debounce delivering traces to registered cbs so that 58 | ;; we can deliver them in batches. This aids us in efficiency 59 | ;; but also importantly lets us avoid slowing down the host 60 | ;; application by running any trace code in the critical path. 61 | ;; 62 | ;; We add a lightweight check on top of goog.functions/debounce 63 | ;; to avoid constant setting and cancelling of timeouts. This 64 | ;; means that we will deliver traces between 10-50 ms from the 65 | ;; last trace being created, which still achieves our goals. 66 | 67 | (def debounce-time 50) 68 | 69 | (defn debounce [f interval] 70 | #?(:cljs (goog.functions/debounce f interval) 71 | :clj (f))) 72 | 73 | (def schedule-debounce 74 | (debounce 75 | (fn tracing-cb-debounced [] 76 | (doseq [[k cb] @trace-cbs] 77 | (try (cb @traces) 78 | #?(:clj (catch Exception e 79 | (console :error "Error thrown from trace cb" k "while storing" @traces e))) 80 | #?(:cljs (catch :default e 81 | (console :error "Error thrown from trace cb" k "while storing" @traces e))))) 82 | (reset! traces [])) 83 | debounce-time)) 84 | 85 | (defn run-tracing-callbacks! [now] 86 | ;; Optimised debounce, we only re-debounce 87 | ;; if we are close to delivery time 88 | ;; to avoid constant setting and cancelling 89 | ;; timeouts. 90 | 91 | ;; If we are within 25 ms of next delivery 92 | (when (< (- @next-delivery 25) now) 93 | (schedule-debounce) 94 | ;; The next-delivery time is not perfectly accurate 95 | ;; as scheduling the debounce takes some time, but 96 | ;; it's good enough for our purposes here. 97 | (reset! next-delivery (+ now debounce-time)))) 98 | 99 | (macros/deftime 100 | (defmacro finish-trace [trace] 101 | `(when (is-trace-enabled?) 102 | (let [end# (interop/now) 103 | duration# (- end# (:start ~trace))] 104 | (swap! traces conj (assoc ~trace 105 | :duration duration# 106 | :end (interop/now))) 107 | (run-tracing-callbacks! end#)))) 108 | 109 | (defmacro with-trace 110 | "Create a trace inside the scope of the with-trace macro 111 | 112 | Common keys for trace-opts 113 | :op-type - what kind of operation is this? e.g. :sub/create, :render. 114 | :operation - identifier for the operation, for a subscription it would be the subscription keyword 115 | :tags - a map of arbitrary kv pairs" 116 | [{:keys [operation op-type tags child-of] :as trace-opts} & body] 117 | `(if (is-trace-enabled?) 118 | (binding [*current-trace* (start-trace ~trace-opts)] 119 | (try ~@body 120 | (finally (finish-trace *current-trace*)))) 121 | (do ~@body))) 122 | 123 | (defmacro merge-trace! [m] 124 | ;; Overwrite keys in tags, and all top level keys. 125 | `(when (is-trace-enabled?) 126 | (let [new-trace# (-> (update *current-trace* :tags merge (:tags ~m)) 127 | (merge (dissoc ~m :tags)))] 128 | (set! *current-trace* new-trace#)) 129 | nil))) 130 | -------------------------------------------------------------------------------- /src/re_frame/utils.cljc: -------------------------------------------------------------------------------- 1 | (ns re-frame.utils 2 | (:require [re-frame.loggers :refer [console]])) 3 | 4 | (defn dissoc-in 5 | "Dissociates an entry from a nested associative structure returning a new 6 | nested structure. keys is a sequence of keys. Any empty maps that result 7 | will not be present in the new structure. 8 | The key thing is that 'm' remains identical? to istelf if the path was never present" 9 | [m [k & ks :as keys]] 10 | (if ks 11 | (if-let [nextmap (get m k)] 12 | (let [newmap (dissoc-in nextmap ks)] 13 | (if (seq newmap) 14 | (assoc m k newmap) 15 | (dissoc m k))) 16 | m) 17 | (dissoc m k))) 18 | 19 | (defn first-in-vector 20 | [v] 21 | (if (vector? v) 22 | (first v) 23 | (console :error "re-frame: expected a vector, but got:" v))) 24 | -------------------------------------------------------------------------------- /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 exercise 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 handler registered for effect::fx-not-exist. Ignoring." (first @logs))) 62 | (is (= (count @logs) 1)) 63 | (finally 64 | (log/set-loggers! original-loggers))))) 65 | -------------------------------------------------------------------------------- /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 :as 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 (partial subs/clear-all-handlers! core/default-frame)}) 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-cache core/default-frame) 28 | restore-fn (make-restore-fn)] 29 | (is (zero? (count original-subs))) 30 | @(subscribe [:test-sub]) 31 | (is (one? (count @(:subs-cache core/default-frame)))) 32 | (is (contains? @(:subs-cache core/default-frame) [[:test-sub] []])) 33 | (restore-fn) 34 | (is (zero? (count @(:subs-cache core/default-frame))))))) 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-cache core/default-frame) 41 | restore-fn (make-restore-fn)] 42 | (is (one? (count original-subs))) 43 | @(subscribe [:test-sub2]) 44 | (is (contains? @(:subs-cache core/default-frame) [[:test-sub2] []])) 45 | (is (two? (count @(:subs-cache core/default-frame)))) 46 | (restore-fn) 47 | (is (not (contains? @(:subs-cache core/default-frame) [[:test-sub2] []]))) 48 | (is (one? (count @(:subs-cache core/default-frame))))))) 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-cache core/default-frame) 55 | restore-fn (make-restore-fn)] 56 | (is (one? (count original-subs))) 57 | @(subscribe [:test-sub :extra :params]) 58 | (is (two? (count @(:subs-cache core/default-frame)))) 59 | (restore-fn) 60 | (is (one? (count @(:subs-cache core/default-frame))))))) 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-cache core/default-frame)))) 68 | restore-fn-2 (make-restore-fn)] 69 | @(subscribe [:test-sub2]) 70 | (is (two? (count @(:subs-cache core/default-frame)))) 71 | (restore-fn-2) 72 | (is (one? (count @(:subs-cache core/default-frame)))) 73 | (restore-fn-1) 74 | (is (zero? (count @(:subs-cache core/default-frame))))))) 75 | -------------------------------------------------------------------------------- /test/re_frame/router_test.clj: -------------------------------------------------------------------------------- 1 | (ns re-frame.router-test 2 | (:require [clojure.test :refer :all] 3 | [re-frame.frame :as frame])) 4 | 5 | (def frame (atom nil)) 6 | 7 | (defn init-frame [] 8 | (reset! frame (doto (frame/make-frame) 9 | (frame/reg-event-db ::test 10 | (fn [db [_ i]] 11 | (update db ::test (fnil conj []) i))) 12 | 13 | (frame/reg-fx ::promise 14 | (fn [{:keys [p val]}] 15 | (deliver p val))) 16 | 17 | (frame/reg-event-fx ::sentinel 18 | (fn [cofx [_ p val]] 19 | {::promise {:p p :val val}}))))) 20 | 21 | (use-fixtures :each {:before init-frame}) 22 | 23 | (deftest dispatching-race-condition-469-test 24 | ;; Checks for day8/re-frame#469 25 | (let [p (promise)] 26 | (is (nil? (dotimes [i 1000] 27 | (frame/dispatch @frame [::test i])))) 28 | (is (nil? (frame/dispatch @frame [::sentinel p ::done]))) 29 | (let [val (deref p 1000 ::timed-out)] 30 | (is (= ::done val))) 31 | (is (= (::test @(:app-db @frame)) 32 | (range 1000))))) 33 | -------------------------------------------------------------------------------- /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 | 13 | (enable-console-print!) 14 | 15 | ;; ---- BROWSER based tests ---------------------------------------------------- 16 | (defn ^:export set-print-fn! [f] 17 | (set! cljs.core.*print-fn* f)) 18 | 19 | 20 | (defn ^:export run-html-tests [] 21 | (cljs-test/run-tests 22 | 're-frame.interceptor-test 23 | 're-frame.subs-test 24 | 're-frame.fx-test 25 | 're-frame.trace-test 26 | 're-frame.restore-test)) 27 | 28 | ;; ---- KARMA ----------------------------------------------------------------- 29 | 30 | (defn ^:export run-karma [karma] 31 | (karma/run-tests 32 | karma 33 | 're-frame.interceptor-test 34 | 're-frame.subs-test 35 | 're-frame.fx-test 36 | 're-frame.trace-test 37 | 're-frame.restore-test)) 38 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------