├── .editorconfig ├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── Bug-Report.yml │ ├── New-Feature.yml │ └── config.yml └── workflows │ ├── continuous-deployment-workflow.yml │ ├── continuous-integration-workflow.yml │ └── docs-workflow.yml ├── .gitignore ├── .idea └── codeStyleSettings.xml ├── .travis.yml ├── CHANGELOG.md ├── CITATION.md ├── CONTRIBUTING.md ├── README.md ├── SUPPORT.md ├── deps.edn ├── docs ├── App-Structure.md ├── Coeffects.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 ├── External-Resources.md ├── FAQs │ ├── BestPractice.md │ ├── DB_Normalisation.md │ ├── DoINeedReFrame.md │ ├── FocusOnElement.md │ ├── FullStackReframe.md │ ├── GlobalInterceptors.md │ ├── Inspecting-app-db.md │ ├── LoadOnMount.md │ ├── Logging.md │ ├── Null-Dispatched-Events.md │ ├── PollADatabaseEvery60.md │ ├── UseASubscriptionInAnEventHandler.md │ ├── ViewsOnGlobalRegistration.md │ ├── When-Does-Dispatch-Happen.md │ ├── Why-CLJC.md │ ├── Why-Clear-Sub-Cache.md │ ├── laggy-input.md │ └── use-cofx-as-fx.md ├── Figma Infographics │ └── inforgraphics.fig ├── Flows.md ├── Interceptors.md ├── Loading-Initial-Data.md ├── Performance-Problems.md ├── README.md ├── Solve-the-CPU-hog-problem.md ├── Subscribing-To-External-Data.md ├── Talking-To-Servers.md ├── Testing.md ├── The-re-frame-logo.md ├── Using-Stateful-JS-Components.md ├── a-loop.md ├── all-models-are-wrong.md ├── api-builtin-effects.md ├── api-intro.md ├── application-state.md ├── breaking-it.md ├── browser-dynamics.md ├── cljdoc.edn ├── clojurescript.md ├── correcting-a-wrong.md ├── data-oriented-design.md ├── deps.edn ├── dominoes-30k.md ├── dominoes-live.md ├── event-handling-infographic.md ├── flow-mechanics.md ├── flows-advanced-topics.md ├── historical.md ├── images │ ├── 404-img.png │ ├── Alien3_0.jpg │ ├── Readme │ │ ├── 6dominoes.png │ │ ├── Dominoes-small.jpg │ │ ├── Dominoes.jpg │ │ └── todolist.png │ ├── delete.me.event-handlers.png │ ├── epoch.png │ ├── event-dispatch.png │ ├── example_app.png │ ├── favicon.png │ ├── handling-one-event.png │ ├── interceptors.png │ ├── logo │ │ ├── Genesis.png │ │ ├── Guggenheim.jpg │ │ ├── README.md │ │ ├── ai │ │ │ ├── Re-frame colour.ai │ │ │ └── Re-frame white.ai │ │ ├── favicon.ico │ │ ├── old │ │ │ ├── f_303w.png │ │ │ ├── frame_1024w.png │ │ │ ├── re-frame-logo.sketch │ │ │ ├── re-frame_128w.png │ │ │ ├── re-frame_256w.png │ │ │ └── re-frame_512w.png │ │ ├── re-frame-colour.png │ │ ├── re-frame-colour.svg │ │ ├── re-frame-white.png │ │ └── re-frame-white.svg │ ├── mental-model-omnibus.jpg │ ├── not-a-domino-lineup.jpg │ ├── not-a-domino.jpg │ ├── scale-changes-everything.jpg │ ├── subscriptions.png │ ├── the-water-cycle.png │ └── yinyang.png ├── index.md ├── interconnections.md ├── on-dynamics.md ├── on-stable-dom-handlers.md ├── re-frame.md ├── reagent.md ├── releases │ ├── 2015.md │ ├── 2016.md │ ├── 2017.md │ ├── 2018.md │ ├── 2019.md │ ├── 2020.md │ ├── 2021.md │ ├── 2022.md │ ├── 2023.md │ └── 2024.md ├── reusable-components.md ├── review-todomvc.md ├── src │ ├── ns_to_markdown.clj │ ├── re_frame │ │ └── docs.cljs │ └── sci │ │ └── configs │ │ ├── re_frame │ │ └── re_frame.cljs │ │ └── reagent │ │ └── reagent.cljs ├── start-coding.md ├── styles │ └── website.css ├── stylesheets │ ├── dominoes-live.css │ └── mkdocs.css ├── subscriptions.md └── theme │ ├── 404.html │ ├── main.html │ ├── overrides │ ├── home.html │ └── main.html │ └── partials │ ├── footer.html │ └── header.html ├── examples ├── flow │ ├── deps.edn │ ├── package-lock.json │ ├── package.json │ ├── resources │ │ └── public │ │ │ ├── example.css │ │ │ └── index.html │ ├── shadow-cljs.edn │ └── src │ │ └── re_frame │ │ └── flow │ │ └── demo.cljs ├── simple │ ├── .gitignore │ ├── README.md │ ├── deps.edn │ ├── package-lock.json │ ├── package.json │ ├── resources │ │ └── public │ │ │ ├── example.css │ │ │ └── index.html │ ├── shadow-cljs.edn │ ├── src │ │ └── simple │ │ │ └── core.cljs │ └── target │ │ └── .gitkeep └── todomvc │ ├── .gitignore │ ├── README.md │ ├── deps.edn │ ├── package-lock.json │ ├── package.json │ ├── resources │ └── public │ │ ├── index.html │ │ └── todos.css │ ├── shadow-cljs.edn │ ├── src │ └── todomvc │ │ ├── core.cljs │ │ ├── db.cljs │ │ ├── events.cljs │ │ ├── subs.cljs │ │ └── views.cljs │ └── target │ └── .gitkeep ├── karma.conf.js ├── license.txt ├── mkdocs.yml ├── package-lock.json ├── package.json ├── project.clj ├── src ├── deps.cljs └── re_frame │ ├── alpha.cljc │ ├── cofx.cljc │ ├── core.cljc │ ├── db.cljc │ ├── events.cljc │ ├── flow │ └── alpha.cljc │ ├── fx.cljc │ ├── interceptor.cljc │ ├── interop.clj │ ├── interop.cljs │ ├── loggers.cljc │ ├── query │ └── alpha.cljc │ ├── register │ └── alpha.cljc │ ├── registrar.cljc │ ├── router.cljc │ ├── settings.cljc │ ├── std_interceptors.cljc │ ├── subs.cljc │ ├── subs │ └── alpha.cljc │ ├── trace.cljc │ └── utils.cljc └── test ├── re_frame ├── event_test.cljs ├── flow │ └── alpha_test.cljc ├── fx_test.cljs ├── interceptor_test.cljs ├── restore_test.cljs ├── router_test.clj ├── subs │ └── alpha_test.cljs ├── subs_test.clj ├── subs_test.cljs ├── test_runner.cljs ├── trace_test.cljs └── utils_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/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: mike-thompson-day8 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug-Report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | title: "[Bug]: " 4 | labels: [bug, triage] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to fill out this bug report! 10 | - type: textarea 11 | id: what-happened 12 | attributes: 13 | label: What happened? 14 | description: Also tell us, what did you expect to happen? 15 | placeholder: Tell us what you see! 16 | value: "A bug happened!" 17 | validations: 18 | required: true 19 | - type: input 20 | id: version 21 | attributes: 22 | label: Version 23 | description: What version of re-frame are you using? 24 | validations: 25 | required: true 26 | - type: dropdown 27 | id: browsers 28 | attributes: 29 | label: What runtimes are you seeing the problem on? 30 | multiple: true 31 | options: 32 | - Mozilla Firefox 33 | - Google Chrome 34 | - Microsoft Edge 35 | - Apple Safari 36 | - Opera 37 | - Brave 38 | - React Native 39 | - Node.js 40 | - JVM (CLJC) 41 | validations: 42 | required: true 43 | - type: textarea 44 | id: logs 45 | attributes: 46 | label: Relevant console output 47 | description: Please copy and paste any relevant console output. This will be automatically formatted into code, so no need for backticks. 48 | render: shell 49 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/New-Feature.yml: -------------------------------------------------------------------------------- 1 | name: Enhancement 2 | description: Suggest a new feature or improvement 3 | title: "[Enhancement]: " 4 | labels: [enhancement, triage] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to suggest a new feature or improvement. A PR would be even better! 10 | - type: textarea 11 | id: suggestion 12 | attributes: 13 | label: What do you suggest? 14 | description: Also tell us, what is your use case for re-frame and how will it improve that use case? 15 | placeholder: Tell us your vision! 16 | value: "Make it better!" 17 | validations: 18 | required: true 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: re-frame GitHub Discussions 4 | url: https://github.com/day8/re-frame/discussions 5 | about: Ask and answer questions in GitHub Flavored Markdown. 6 | - name: "#re-frame Clojurians Slack Chat" 7 | url: https://clojurians.slack.com 8 | about: Active channel for real-time discussion, questions and answers. Clojurians is a Slack Pro account Sponsored by Slack Technologies, LLC. 9 | - name: "Stack Overflow Questions tagged [re-frame]" 10 | url: https://stackoverflow.com/questions/tagged/re-frame 11 | about: Stack Overflow 12 | -------------------------------------------------------------------------------- /.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-latest 20 | container: 21 | # Source: https://github.com/day8/dockerfiles-for-dev-ci-images 22 | image: ghcr.io/day8/chrome-56:5.0.0 23 | credentials: 24 | username: ${{ github.actor }} 25 | password: ${{ secrets.GITHUB_TOKEN }} 26 | steps: 27 | - uses: actions/checkout@v2 28 | - run: git config --system --add safe.directory /__w/re-frame/re-frame 29 | - name: Maven cache 30 | id: maven-cache 31 | uses: actions/cache@v4 32 | with: 33 | path: /root/.m2/repository 34 | key: ${{ runner.os }}-maven-${{ hashFiles('**/project.clj', '.github/workflows/**') }} 35 | restore-keys: | 36 | ${{ runner.os }}-maven- 37 | - name: npm cache 38 | uses: actions/cache@v4 39 | with: 40 | path: ~/.npm 41 | key: ${{ runner.os }}-npm-${{ hashFiles('project.clj') }}-${{ hashFiles('**/deps.cljs') }} 42 | restore-keys: | 43 | ${{ runner.os }}-npm- 44 | - name: shadow-cljs compiler cache 45 | uses: actions/cache@v4 46 | with: 47 | path: .shadow-cljs 48 | key: ${{ runner.os }}-shadow-cljs-${{ github.sha }} 49 | restore-keys: | 50 | ${{ runner.os }}-shadow-cljs- 51 | - if: steps.maven-cache.outputs.cache-hit != 'true' 52 | run: lein ci 53 | - if: steps.maven-cache.outputs.cache-hit != 'true' 54 | name: Run cd examples/simple && lein shadow-release && lein build-report 55 | working-directory: examples/simple 56 | run: | 57 | npm install 58 | npm run release 59 | npm run build-report 60 | - if: steps.maven-cache.outputs.cache-hit != 'true' 61 | name: Run cd examples/todomvc && lein shadow-release && lein build-report 62 | working-directory: examples/todomvc 63 | run: | 64 | npm install 65 | npm run release 66 | npm run build-report 67 | - if: steps.maven-cache.outputs.cache-hit == 'true' 68 | run: lein -o ci 69 | - if: steps.maven-cache.outputs.cache-hit == 'true' 70 | name: Run cd examples/simple && lein -o shadow-release && lein -o build-report 71 | working-directory: examples/simple 72 | run: | 73 | npm install 74 | npm run release 75 | npm run build-report 76 | - if: steps.maven-cache.outputs.cache-hit == 'true' 77 | name: Run cd examples/todomvc && lein -o shadow-release && lein -o build-report 78 | working-directory: examples/todomvc 79 | run: | 80 | npm install 81 | npm run release 82 | npm run build-report 83 | - uses: actions/upload-artifact@v4 84 | with: 85 | name: simple-example-build-report 86 | path: examples/simple/target/build-report.html 87 | - uses: actions/upload-artifact@v4 88 | with: 89 | name: todomvc-example-build-report 90 | path: examples/todomvc/target/build-report.html 91 | - name: Verify cljdoc Configuration 92 | run: curl -fsSL https://raw.githubusercontent.com/cljdoc/cljdoc/master/script/verify-cljdoc-edn | bash -s docs/cljdoc.edn 93 | - name: Slack notification 94 | uses: homoluctus/slatify@v2.0.1 95 | if: failure() || cancelled() 96 | with: 97 | type: ${{ job.status }} 98 | job_name: re-frame Tests 99 | channel: '#oss-robots' 100 | url: ${{ secrets.SLACK_WEBHOOK }} 101 | commit: true 102 | token: ${{ secrets.GITHUB_TOKEN }} 103 | -------------------------------------------------------------------------------- /.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 | /examples/flow/resources/public/js/ 21 | /docs/js 22 | .floo 23 | .flooignore 24 | node_modules/ 25 | examples/todomvc/.idea/ 26 | docs/build/_book/ 27 | docs/build/index.html 28 | docs/**/*.epub 29 | docs/**/*.pdf 30 | docs/**/*.mobi 31 | build/ 32 | *~ 33 | /shadow-cljs.edn 34 | /.shadow-cljs/ 35 | .shadow-cljs 36 | /site/ 37 | .cpcache 38 | .lsp 39 | .clj-kondo -------------------------------------------------------------------------------- /.idea/codeStyleSettings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: bionic 2 | os: linux 3 | language: clojure 4 | cache: 5 | directories: 6 | - "$HOME/.m2" 7 | before_install: 8 | - npm i -g npm 9 | - yes y | sudo lein upgrade 10 | - curl -sSL https://raw.githubusercontent.com/cljs-oss/canary/master/scripts/install-canary.sh | bash 11 | script: 12 | - lein ci 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | The [CHANGELOG has moved here](https://day8.github.io/re-frame/releases/2024). 2 | 3 | -------------------------------------------------------------------------------- /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 | To run the tests, you must have recent versions of node, npm and Leiningen. 18 | 19 | To build the tests and run them in one step via karma, just: 20 | ```sh 21 | npm install -g karma-cli 22 | lein ci 23 | ``` 24 | 25 | You can also get auto compiles via: 26 | ```sh 27 | lein watch 28 | ``` 29 | then open [http://localhost:3449/](http://localhost:3449/) in a browser for an auto-reloaded browser-based test runner. 30 | 31 | ## Pull requests for bugs 32 | 33 | If possible provide: 34 | 35 | * Code that fixes the bug 36 | * Failing tests which pass with the new changes 37 | * Improvements to documentation to make it less likely that others will run into issues (if relevant). 38 | * Add the change to the Unreleased section of [CHANGELOG.md](docs/CHANGELOG.md) 39 | 40 | ## Pull requests for features 41 | 42 | If possible provide: 43 | 44 | * Code that implements the new feature 45 | * Tests to cover the new feature including all of the code paths 46 | * Docstrings for functions 47 | * Documentation examples 48 | * Add the change to the Unreleased section of [CHANGELOG.md](docs/CHANGELOG.md) 49 | 50 | ## Pull requests for docs 51 | 52 | Please see the [docs/README.md](docs/README.md) 53 | 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |

re-frame logo

4 | 5 | ## Derived Values, Flowing 6 | 7 | > This, milord, is my family's axe. We have owned it for almost nine hundred years, see. Of course, 8 | sometimes it needed a new blade. And sometimes it has required a new handle, new designs on the 9 | metalwork, a little refreshing of the ornamentation ... but is this not the nine hundred-year-old 10 | axe of my family? And because it has changed gently over time, it is still a pretty good axe, 11 | y'know. Pretty good. 12 | 13 | > -- Terry Pratchett, The Fifth Elephant
14 | >     reflecting on identity, flow and derived values (aka [The Ship of Theseus](https://en.wikipedia.org/wiki/Ship_of_Theseus)) 15 |
16 |
17 | 18 | 23 | 24 | ## Overview 25 | 26 | re-frame is a ClojureScript framework for building user interfaces. 27 | It has a data-oriented, functional design. Its primary focus is on high programmer productivity and scaling up to larger Single-Page applications. 28 | 29 | Developed in late 2014, and released in 2015, it is mature and stable. It is used by both small startups and companies with over 500 developers, and it has delivered into production applications which are 40K lines of code and beyond. 30 | 31 | Across the last 6 years, it has outlasted multiple generations of Javascript churn - just imagine your team's productivity if you didn't have to contend with technical churn, and have new magic burn your fingers every two years. Brand new, exciting concepts like recoiljs (in the React world), have been a regular part of re-frame from the beginning. 32 | 33 | re-frame is lucky enough to enjoy an unfair advantage - ClojureScript is a Lisp. Alan Kay 34 | once described Lisp as "Maxwell's equations of software". Paul Graham 35 | described how Lisp was a competitive advantage for his startup. When we use Lisp, we 36 | get to leverage 50 years of foliated excellence from the very best minds available. 37 | And then there's also a thriving ClojureScript community which delivers modern ideas and best-in-class tooling. 38 | 39 | Although re-frame leverages React (via Reagent), it only needs 40 | React to be the V in MVC, and no more. re-frame takes a different road to the currently-pervasive idea that Views should be causal (colocated queries, ComponentDidMount, hooks, etc). 41 | In re-frame, events are causal, and views are purely reactive. 42 | 43 | ## Documentation 44 | 45 | The re-frame documentation is [available here](https://day8.github.io/re-frame/). 46 | 47 | 48 | ## The Current Version 49 | 50 | [![Clojars Project](https://img.shields.io/clojars/v/re-frame?labelColor=283C67&color=729AD1&style=for-the-badge&logo=clojure&logoColor=fff)](https://clojars.org/re-frame) 51 | 52 | For full dependency information, see the [Clojars page](https://clojars.org/re-frame/) 53 | 54 | ## Getting Help 55 | 56 | [![Get help on Slack](http://img.shields.io/badge/slack-clojurians%20%23re--frame-97C93C?labelColor=283C67&logo=slack&style=for-the-badge)](https://clojurians.slack.com/channels/re-frame) 57 | 58 | ## Licence 59 | 60 | re-frame is [MIT licenced](license.txt) 61 | 62 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | 2 | ;; Allows users of deps.edn to more conveniently fork / make PRs to re-frame 3 | 4 | {:deps {org.clojure/clojure {:mvn/version "1.10.3" 5 | :scope "provided"} 6 | org.clojure/clojurescript {:mvn/version "1.10.844" 7 | :scope "provided"} 8 | reagent/reagent {:mvn/version "1.2.0"} 9 | net.cgrand/macrovich {:mvn/version "0.2.1"} 10 | org.clojure/tools.logging {:mvn/version "1.1.0"}}} 11 | -------------------------------------------------------------------------------- /docs/App-Structure.md: -------------------------------------------------------------------------------- 1 | ## A Smaller App 2 | 3 | For simpler apps, you should put code for each layer into separate files: 4 |
  5 | src
  6 | ├── core.cljs         <--- entry point, plus history, routing, etc
  7 | ├── db.cljs           <--- schema, validation, etc  (data layer)
  8 | ├── views.cljs        <--- reagent views (view layer)
  9 | ├── events.cljs       <--- event handlers (control/update layer)
 10 | └── subs.cljs         <--- subscription handlers  (query layer)
 11 | 
12 | 13 | For a living example of this approach, look at the [todomvc example](https://github.com/day8/re-frame/tree/master/examples/todomvc). 14 | 15 | ## The Gotcha 16 | 17 | If you adopt this structure, there's a gotcha. 18 | 19 | `events.cljs` and `subs.cljs` will never be `required` by any other 20 | namespaces. To the Google Closure dependency mechanism, it appears as 21 | if these two namespaces are not needed and it doesn't load them. 22 | 23 | And, if the namespaces are not loaded, the registrations in these namespaces will 24 | never happen. And, then you'll be staring at your running app very 25 | puzzled about why none of your events handlers are registered. 26 | 27 | Once you twig to what's going on, the solution is easy. You must 28 | explicitly `require` both namespaces, `events` and `subs`, in your `core` 29 | namespace. Then they'll be loaded and the registrations (`reg-sub`, `reg-event-fx`, 30 | etc) will occur as that loading happens. 31 | 32 | ## Larger Apps 33 | 34 | Assuming your larger apps have multiple "panels" (or "views") which are 35 | relatively independent, you might use this structure: 36 |
 37 | src
 38 | ├── core.cljs             <--- entry point, plus history, routing, etc
 39 | ├── panel-1
 40 | │   ├── db.cljs           <--- schema, validation, etc  (data layer)
 41 | │   ├── subs.cljs         <--- subscription handlers  (query layer)
 42 | │   ├── views.cljs        <--- reagent components (view layer)
 43 | │   └── events.cljs       <--- event handlers (control/update layer)
 44 | ├── panel-2
 45 | │   ├── db.cljs           <--- schema, validation. etc  (data layer)
 46 | │   ├── subs.cljs         <--- subscription handlers  (query layer)
 47 | │   ├── views.cljs        <--- reagent components (view layer)
 48 | │   └── events.cljs       <--- event handlers (control/update layer)
 49 | .
 50 | .
 51 | └── panel-n
 52 | 
53 | 54 | 55 | ## Namespaced Ids 56 | 57 | As an app gets bigger, you'll tend to get clashes on ids - event-ids, or query-ids (subscriptions), etc. 58 | 59 | One panel will need to `dispatch` an `:edit` event and so will 60 | another, but the two panels will have different handlers. 61 | So, how do you avoid a clash? How do you distinguish between 62 | one `:edit` event and another? 63 | 64 | Your goal should be to use event-ids which encode both the event 65 | itself (`:edit` ?) and the context (`:panel1` or `:panel2` ?). 66 | 67 | Luckily, ClojureScript provides a nice easy solution: use keywords 68 | with a __synthetic namespace__. Perhaps something like `:panel1/edit` and `:panel2/edit`. 69 | 70 | You see, ClojureScript allows the namespace in a keyword to be a total 71 | fiction. I can have the keyword `:panel1/edit` even though 72 | `panel1.cljs` doesn't exist. 73 | 74 | Naturally, you'll take advantage of this by using keyword namespaces 75 | which are both unique and descriptive. 76 | 77 | ## Navigation 78 | 79 | 80 | How do I switch between different panels of a larger app? 81 | 82 | Your `app-db` could have an `:active-panel` key containing an id for the panel being displayed. 83 | 84 | 85 | 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: 86 | 87 | ```clj 88 | (re-frame/reg-event-db 89 | :set-active-panel 90 | (fn [db [_ value]] 91 | (assoc db :active-panel value))) 92 | 93 | (re-frame/dispatch 94 | [:set-active-panel :panel1]) 95 | ``` 96 | 97 | A high level reagent view has a subscription to :active-panel and will switch to the associated panel. 98 | 99 | ```clj 100 | (re-frame/reg-sub 101 | :active-panel 102 | (fn [db _] 103 | (:active-panel db))) 104 | 105 | (defn panel1 106 | [] 107 | [:div {:on-click #(re-frame/dispatch [:set-active-panel :panel2])} 108 | "Here" ]) 109 | 110 | (defn panel2 111 | [] 112 | [:div "There"]) 113 | 114 | (defn high-level-view 115 | [] 116 | (let [active (re-frame/subscribe [:active-panel])] 117 | (fn [] 118 | [:div 119 | [:div.title "Heading"] 120 | (condp = @active ;; or you could look up in a map 121 | :panel1 [panel1] 122 | :panel2 [panel2])]))) 123 | ``` 124 | -------------------------------------------------------------------------------- /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 supplying 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 | - animation? CSS ? 26 | 27 | XXX searches up the DOM hierarchy 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 or 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/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/FAQs/BestPractice.md: -------------------------------------------------------------------------------- 1 | 4 | # 5 | 6 | 7 | ## Question 8 | 9 | What are the current best practices? 10 | 11 | ## Answer 12 | 13 | To grasp best practices: 14 | 15 | 1. Read through the re-frame documentation. 16 | 2. Review example projects, like this [RealWorld example](https://github.com/jacekschae/conduit). The [Resources doc](http://day8.github.io/re-frame/External-Resources/#examples-and-applications-using-re-frame) provides further examples. 17 | 18 | Keep in mind, best practices evolve over time. Here are some of the more recent ideas. 19 | 20 | ### Use Namespaced Keywords 21 | 22 | In the interests of minimalism, the docs don't use namespaced keywords for ids (event ids, subscription ids, etc), but in prectice you should. 23 | 24 | You can use either synthetic or real namespaces in your ids, but some experienced re-framers claim you should only use real namespaces. I remain unconvinced. Shrug. 25 | 26 | Tip: tooling like `clojure-lsp` can help you to navigate to where an event id or subsription id is registered. 27 | 28 | ### Structuring `app-db` 29 | 30 | While using `app-db` as a simple map works well in many situations, if you want more structure, consider using a library like [doxa](https://github.com/ribelo/doxa) or [relic](https://github.com/wotbrew/relic). 31 | 32 | ### Use the `:fx` effect 33 | 34 | While event handlers can return a map of arbitrary effects, it is now recommended that they only return 35 | a map containing two standard keys `:db` and `:fx`. Learn more [here](https://day8.github.io/re-frame/api-builtin-effects/#fx) and [here](http://day8.github.io/re-frame/releases/2020/#110-2020-08-24). 36 | 37 | ### Compose Event Handlers 38 | 39 | Event handlers can be composed of other functions through the use of the `:db` / `:fx` effect pattern, which is described [here](https://github.com/day8/re-frame/issues/639#issuecomment-682250517) 40 | 41 | ### Avoid Placeful Events 42 | 43 | Originally, it was recommended that events be a vector like this `[:some-event-id arg1 arg2]`. This works reasonably for simple cases, but it does introduce fragility for more complex use cases due to the lnherent "placefulness" of vectors. 44 | 45 | A better practice is to encapsulate the "args" into a single map: `[:some-event-id {:x arg1 :another arg2}]` 46 | 47 | And then to optionally use the `unwrap` middleware on the associated event handlers. See [here](http://day8.github.io/re-frame/api-re-frame.core/#unwrap) 48 | 49 | -------------------------------------------------------------------------------- /docs/FAQs/DB_Normalisation.md: -------------------------------------------------------------------------------- 1 | 2 | 5 | # 6 | 7 | ## Question 8 | 9 | `app-db` contains a `map`. How do I store normalised data in a `map`, 10 | bettering mirroring the structure in my server-side database? 11 | 12 | ## Answer 13 | 14 | These libraries might be interesting to you: 15 | 16 | - [compound](https://github.com/riverford/compound) 17 | - [SubGraph](https://github.com/vimsical/subgraph) 18 | - [pull](https://github.com/juxt/pull) 19 | - [reflet](https://github.com/zalky/reflet) - use normalised and non-normalised data in the same `app-db` 20 | 21 | See also [this comment](https://github.com/day8/re-frame/issues/304#issuecomment-269620609). 22 | 23 | If you want to go whole hog and ditch `app-db` and embrace DataScript, 24 | then you might find [re-posh](https://github.com/denistakeda/re-posh) interesting. 25 | -------------------------------------------------------------------------------- /docs/FAQs/DoINeedReFrame.md: -------------------------------------------------------------------------------- 1 | 2 | 5 | # 6 | 7 | ## Question 8 | 9 | Reagent looks terrific. So, why do I need re-frame? What benefit 10 | is there in the extra layers and conceptual overhead it brings? 11 | 12 | ## Answer 13 | 14 | Yes, we agree, Reagent is terrific. We use it enthusiastically ourselves. And, yes, we'd agree that if your application 15 | is small and simple, then standalone Reagent is a reasonable choice. 16 | 17 | But it does only supply the V part of the MVC triad. As your application 18 | gets bigger and more complicated, you *will* need to find solutions to 19 | questions in the M and C realms. 20 | 21 | Questions like "where do I put control logic?". 22 | And, "how do I store and update state?". 23 | And, "how should new websocket packets be communicated with the broader app"? Or GET failures? 24 | And, "how do I put up a spinner 25 | when waiting for CPU intensive computations to run, while allowing the user to press Cancel?" 26 | How do I ensure efficient view updates? How do I write my control logic in a way that's testable? 27 | 28 | These questions accumulate. 29 | 30 | Reagent, by itself, provides little guidance and, so, you'll need to 31 | design your own solutions. Your choices will also accumulate and, 32 | over time, they'll become baked into your codebase. 33 | 34 | Now, any decision which is hard to revisit later is an architectural decision - 35 | "difficult to change later" is pretty much the definition of "architecture". So, 36 | as you proceed, baking your decisions into your codebase, you will be 37 | incrementally growing an architecture. 38 | 39 | So, then, the question is this: is your architecture better than re-frame's? Because 40 | that's what re-frame gives you ... an architecture ... solutions to the 41 | various challenges you'll face when developing your app, and mechanism for implementing 42 | those solutions. 43 | 44 | Now, in response, some will enthusiastically say "yes, I want to grow my own 45 | architecture. I like mine!". And fair enough - it can be an interesting ride! 46 | 47 | Problems arise ONLY when this process is not conscious and purposeful. It is a 48 | credit to Reagent that you can accelerate quickly and get a bunch of enjoyable 49 | early wins. But, equally, that acceleration can have you off the road 50 | in a ditch because of the twists and turns on the way to a larger application. 51 | 52 | I've had many people (20?) privately say to me that's what happened to them. 53 | And that's pretty much the reason for this FAQ - this happens a bit too often 54 | and there's been a bit too much pain. 55 | 56 | So, my advice is ... if your application is a little more complicated, 57 | be sure to make a conscious choice around architecture. Don't think 58 | "Reagent is all I need", because it isn't. One way or 59 | another you'll be using "Reagent + a broader architecture". 60 | 61 | ## Example Choices Made By re-frame 62 | 63 | 1. Events are cardinal. Nothing happens without an event. 64 | 2. Events are data (so they are loggable, and can be queued, etc). 65 | 3. Dispatched events are handled async - they are put in a queue and handled very soon, but not now (for a variety of subtle reasons). 66 | 4. For efficiency, subscriptions (reactions) should be layered and de-duplicated. 67 | 5. Views are never imperative or side effecting (best effort). 68 | 6. Unidirectional data flow only, ever. 69 | 7. Interceptors over middleware. Provide cross cutting concerns like logging and debugging. 70 | 8. Event handlers capture control and contain key logic. Ensure purity via coeffects and effects. 71 | 9. All state is stored in one place. 72 | 10. State is committed-to transactionally, never incrementally or piecemeal. 73 | 74 | Hmm. I feel like I'm missing a few, but that's certainly an indicative list. 75 | 76 | re-frame is only about 750 lines of code. So its value is much more in the honed 77 | choices it embodies (and documents), than the code it provides. 78 | 79 | ## What Reagent Does Provide 80 | 81 | Above I said: 82 | > Reagent, by itself, provides little guidance ... 83 | 84 | which is true but, it does provide useful building blocks. If you do want to create 85 | your own architecture, then be sure to check out Reagent's `track`, `reaction` and `rswap`. 86 | 87 | There's also other Reagent-based architectures like [keechma](https://github.com/keechma/keechma) and 88 | [carry](https://github.com/metametadata/carry) which make different choices - ones which may 89 | better suit your needs. 90 | -------------------------------------------------------------------------------- /docs/FAQs/FocusOnElement.md: -------------------------------------------------------------------------------- 1 | 2 | 5 | # 6 | 7 | ## Question 8 | 9 | How do I switch "focus" to a particular HTML element? 10 | 11 | ## HTML autofocus 12 | 13 | Perhaps you can use the `autofocus` HTML element attribute like this: 14 | ```cljs 15 | (defn view 16 | [] 17 | [:input {:type "text" :id "my-id" :auto-focus true]) 18 | ``` 19 | 20 | But this might not work in Safari these days (Safari is the new IE 6 of browsers). 21 | 22 | Instead, you could use a more portable (but more complicated) version of this approach, which uses React `refs` with a Form-3 component: 23 | ```clj 24 | (defn my-input [] 25 | (let [ref (atom nil)] 26 | (r/create-class 27 | {:component-did-mount 28 | (fn [_] 29 | (.focus @ref)) 30 | :reagent-render 31 | (fn [_] 32 | [:input {:ref #(reset! ref %)}])}))) 33 | ``` 34 | 35 | A terse way of achieving the same outcome is: 36 | ```clj 37 | [:input {:ref #(when % (.focus %)}] 38 | ``` 39 | 40 | But all these approaches only cause focus once, when the widget is first rendered. You may need to have more control than that. 41 | 42 | ## Reagent after-render 43 | 44 | If you want to switch focus between elements after they have first rendered, 45 | you can create an `effect handler` which uses Reagent's `after-render` API to 46 | register a function that will imperatively set focus: 47 | ```clj 48 | (re-frame.core/reg-fx 49 | :focus-to-element 50 | (fn [element-id] 51 | (reagent/after-render #(some-> js/document (.getElementById element-id) .focus)))) 52 | ``` 53 | _WARNING_: as written, this code will fail silently if `element-id` is not found. If you use this 54 | code fragment, you may want to detect and report that problem. 55 | 56 | You can then use this effect within your event handler: 57 | ```clj 58 | (re-frame.core/reg-event-fx 59 | :something 60 | (fn [cofx event] 61 | {:db .... 62 | :focus-to-element some-element-id})) 63 | ``` 64 | 65 | This assumes you can compute or obtain the `some-element-id` value 66 | for the HTML element on which you want focus. 67 | 68 | One small trick: we perform the imperative focus using 69 | `Reagent/after-render` because sometimes the target 70 | HTML element won't exist in the DOM until after the rendering 71 | which occurs in the next animation frame. 72 | -------------------------------------------------------------------------------- /docs/FAQs/FullStackReframe.md: -------------------------------------------------------------------------------- 1 | 2 | 5 | # 6 | 7 | ## Question 8 | 9 | I'm interested in taking the re-frame concepts and applying them to 10 | my entire Client/Server stack. 11 | 12 | ## The Short Answer 13 | 14 | You'll want to investigate CQRS. 15 | 16 | ## The Longer Answer 17 | 18 | 1. Perhaps watch [Bobby Calderwood's video](https://www.youtube.com/watch?v=B1-gS0oEtYc)? 19 | 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). 20 | 4. Be aware that "Event Sourcing" often comes along for the ride 21 | with CQRS, but it doesn't have to. It adds complexity (Kafka?). 22 | Don't do it lightly. Maybe just use CQRS without ES? 23 | 5. If you do want Event Sourcing, then Kafka might be your friend, 24 | Greg Young might be your God and [Onyx](https://github.com/onyx-platform/onyx) 25 | may be useful. 26 | 27 | ## Further Related Links 28 | 29 | * Reactive PostgreSQL: 30 | https://yogthos.net/posts/2016-11-05-LuminusPostgresNotifications.html 31 | * Datalog All The Way Down: 32 | https://www.youtube.com/watch?v=aI0zVzzoK_E 33 | -------------------------------------------------------------------------------- /docs/FAQs/GlobalInterceptors.md: -------------------------------------------------------------------------------- 1 | 2 | 5 | # 6 | 7 | ## Question 8 | 9 | Does re-frame allow me to register global interceptors? Ones which are included 10 | for every event handler? 11 | 12 | ## Answer (v1.0.0 onwards) 13 | 14 | Yes, re-frame provides an API for registering global interceptors. 15 | 16 | The following code creates a global interceptor to keep a track of all events: 17 | 18 | ```clj 19 | ;; We'll be recording events into this atom 20 | ;; The most recent events will be at the front of the list. 21 | (def event-store (atom (list))) 22 | 23 | 24 | (defn keep-last-20 25 | [existing new-one] 26 | (take 20 (conj existing new-one))) 27 | 28 | 29 | ;; this interceptor will collect events and add them to the atom above 30 | (def event-collector 31 | (re-frame.core/->interceptor 32 | :id :event-collector 33 | :before (fn [context] 34 | (swap! event-store keep-last-20 (re-frame.core/get-coeffect context :event)) 35 | context))) 36 | 37 | ;; register this global interceptor early in program's boot process, 38 | ;; using re-frame's API 39 | (re-frame.core/reg-global-interceptor event-collector) 40 | ``` 41 | 42 | 43 | ## Answer (prior to v1.0.0) 44 | 45 | Prior to v1.0.0, re-frame provided no API to directly support this feature, 46 | but there are ways of making it happen. 47 | 48 | Let's assume you have an interceptor called `omni-ceptor` which you want 49 | automatically added to all your event handlers. 50 | 51 | You'd write a replacement for both `reg-event-db` and `reg-event-fx`, and get 52 | these replacements to automatically add `omni-ceptor` to the interceptor 53 | chain at registration time. 54 | 55 | Here's how to write one of these auto-injecting replacements: 56 | ```clj 57 | (defn my-reg-event-db ;; a replacement for reg-event-db 58 | 59 | ;; 2-arity with no interceptors 60 | ([id handler] 61 | (my-reg-event-db id nil handler)) 62 | 63 | ;; 3-arity with interceptors 64 | ([id interceptors handler] 65 | (re-frame.core/reg-event-db ;; which uses reg-event-db 66 | id 67 | [omni-ceptor interceptors] ;; <-- inject `omni-ceptor` 68 | handler))) 69 | ``` 70 | 71 | NB: did you know that interceptor chains are flattened and nils are removed? 72 | 73 | With this in place, you would always use `my-reg-event-db` 74 | instead of the standard `reg-event-db`: 75 | ```clj 76 | (my-reg-event-db 77 | :event-id 78 | (fn [db v] 79 | ...)) 80 | ``` 81 | 82 | And, hey presto, you'd have your `omni-ceptor` "globally" injected. 83 | -------------------------------------------------------------------------------- /docs/FAQs/Inspecting-app-db.md: -------------------------------------------------------------------------------- 1 | 2 | 5 | # 6 | 7 | ## Question 8 | 9 | How can I inspect the contents of `app-db`? Perhaps from figwheel. 10 | 11 | ## Short Answer 12 | 13 | If at a REPL, inspect: `re-frame.db/app-db`. 14 | 15 | If at the js console, that's `window.re_frame.db.app_db.state`. 16 | 17 | 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). 18 | 19 | You are [using cljs-devtools](https://github.com/binaryage/cljs-devtools), right? 20 | If not, stop everything ([unless you are using re-natal](https://github.com/drapanjanas/re-natal/issues/137)) and immediately make that happen. 21 | 22 | ## Better Answer 23 | 24 | Are you sure you need to? 25 | 26 | First, you seldom want to inspect all of `app-db`. 27 | And, second, inspecting via a REPL might be clumsy. 28 | 29 | Instead, you probably want to inspect a part of `app-db`. __And__ you probably want 30 | to inspect it directly in the GUI itself, not off in a REPL. 31 | 32 | Here is a useful technique from @escherize. Add something like this to 33 | the hiccup of your view ... 34 | ```clj 35 | [:pre (with-out-str (pprint @interesting))] 36 | ``` 37 | This assumes that `@interesting` is the value (ratom or subscription) 38 | you want to observe (note the @ in front). 39 | 40 | `pprint` output is nice to read, but not compact. For a more compact view, do this: 41 | ```clj 42 | [:pre (pr-str @some-atom)] ;; NB: using pr-str instead of pprint 43 | ``` 44 | 45 | If you choose to use `pprint` then you'll need to `require` it within the `ns` of your `view.cljs`: 46 | ```clj 47 | [cljs.pprint :refer [pprint]] 48 | ``` 49 | 50 | Finally, combining the short and long answers, you could even do this: 51 | ```clj 52 | [:pre (with-out-str (pprint @re-frame.db/app-db))] ;; see everything! 53 | ``` 54 | or 55 | ```clj 56 | [:pre (with-out-str (pprint (:part @re-frame.db/app-db)))] ;; see a part of it! 57 | ``` 58 | 59 | You definitely have [clj-devtools](https://github.com/binaryage/cljs-devtools) installed now, right? 60 | 61 | ## Other Inspection Tools 62 | 63 | Another very strong tool is [re-Frisk](https://github.com/flexsurfer/re-frisk) which 64 | provides a nice solution for navigating and inspecting your re-frame data structures. 65 | 66 | @yogthos' [json-html library](https://github.com/yogthos/json-html) provides 67 | a slick presentation, at the expense of more screen real estate, and the 68 | need to include specific CSS. 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /docs/FAQs/LoadOnMount.md: -------------------------------------------------------------------------------- 1 | 2 | 5 | # 6 | 7 | ## Question 8 | 9 | How do I load the data for a view, when the user navigates to that view? 10 | 11 | ## For Javascript The Developer 12 | 13 | Are you from the Javascript/React world? If so, this first section is for you. 14 | 15 | In re-frame, Components are functions. They compute a materialized view of values which are reactively delivered, and they don't have state. 16 | 17 | On the other hand, React has a more OO sense of Components. They are classes with state and behaviour. And even React's more modern, so-called function components come with Hooks which are ordered, and often imperative and effectful. 18 | 19 | So, with respect to Components, re-frame and React are technically different. But it is on a philosophical point that they differ the most: 20 | 21 | - In re-frame, **Components are not causal**, they are reactive. 22 | - Whereas React Components are often deeply causal, via Collocated queries, ComponentDidMount, Hooks and Suspense, etc. In React, Components initiate things - like HTTP requests. 23 | - In re-frame, ***it is events which are causal*** (never components). 24 | 25 | ### Why this difference? 26 | 27 | > Humans have a cognitive bias: "what is focal is presumed causal". 28 | 29 | Political leaders know this. They like presenting good news themselves because they like to 30 | be "seen" as causal of good stuff, but they'll get a press secretary to deliver bad news. 31 | Movie directors know how to use this when framing their protagonists within the story. 32 | 33 | Unfortunately, the React team have lost themselves in this bias. They keep trying to make the 34 | most focal part of the system (components) also be the causal part. Please stop doing that! It is a mistake. Events are what's causal - they embody the user's intent. 35 | 36 | Just to be clear, I love React. What an utterly brilliant idea and great execution. 37 | I'm deeply grateful because, wow!, did it change things. 38 | It is just that I preferred React when it was only trying to be the V part of MVC. Everything since has been downhill. 39 | 40 | Perhaps 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". 41 | 42 | ## Do This Instead 43 | 44 | With re-frame, imperative, effectful stuff only ever happens during the handling of an event. 45 | 46 | When the user clicks on a tab to change what is shown 47 | to them in the UI, an event is dispatched, and it is 48 | the associated event handler which will compute the 49 | effects of the user's request. That might include: 50 | 51 | 1. change application state so the panel is shown 52 | 2. further change application state so that a "twirly busy" thing is shown 53 | 3. issue a database query or open a websocket 54 | 55 | Also, remember that events should model "user intent", like 56 | "review overdue items". Be sure to never model events like 57 | "load overdue items from database" because that's just a 58 | low level operation performed in the service of fulfilling 59 | the user's intent. 60 | 61 | There's a useful effect handler available for HTTP work: 62 | 63 | 64 | Look at the "Real World App" example for inspiration: 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /docs/FAQs/Logging.md: -------------------------------------------------------------------------------- 1 | 2 | 5 | # 6 | 7 | ## Question 8 | 9 | I use logging method X, how can I make re-frame use my method? 10 | 11 | ## Answer 12 | 13 | re-frame makes use of the logging functions: `warn`, `log`, `error`, `group` and `groupEnd`. 14 | 15 | By default, these functions map directly to the js/console equivalents, but you can 16 | override that by providing your own set or subset of these functions using 17 | `re-frame.core/set-loggers!` like this: 18 | ```clj 19 | (defn my-warn 20 | [& args] 21 | (post-warning-somewhere (apply str args))) 22 | 23 | (defn my-log 24 | [& args] 25 | (write-to-datadog (apply str args))) 26 | 27 | (re-frame.core/set-loggers! {:warn my-warn 28 | :log my-log 29 | ...}) 30 | ``` 31 | -------------------------------------------------------------------------------- /docs/FAQs/Null-Dispatched-Events.md: -------------------------------------------------------------------------------- 1 | 2 | 5 | # 6 | 7 | ## Question 8 | 9 | If I `dispatch` a js event object (from a view), it is nullified 10 | by the time it gets to the event-handler. What gives? 11 | 12 | ```cljs 13 | :on-click (fn [event] (dispatch [:clicked event])) 14 | ``` 15 | 16 | ## Short Answer 17 | 18 | If you want to `dispatch` a js event object to a re-frame 19 | event handler, you must call `(.persist event)` before the `dispatch`. 20 | React recycles events (using a pool), and re-frame event handlers 21 | run async. [Find out more here](https://facebook.github.io/react/docs/events.html) 22 | 23 | 24 | ## Longer Answer 25 | 26 | It is better to extract the salient details from the event 27 | and `dispatch` them, rather than the entire js event object. When you 28 | `dispatch` pure, simple ClojureScript data (ie. rather than js objects) testing 29 | and debugging will be easier. 30 | 31 | To put this point even more strongly again, think about it like this: 32 | 33 | - a DOM `on-click` `callback` might tell us "a button was clicked" 34 | - our application must then interpret that click. The click means 35 | the user wanted to achieve something. They had "intent". 36 | - it is this "intent" which should be captured in the re-frame `event` 37 | which is dispatched. It is this intent which the event handler must 38 | later facilitate. 39 | 40 | 41 | So, in summary, re-frame view functions should transform DOM events 42 | into re-frame `events` which capture user intent: "a button was clicked" 43 | becomes `user wants to delete item with id 42` 44 | 45 | As a result, philosophically, low-level DOM objects have no place in an event. 46 | -------------------------------------------------------------------------------- /docs/FAQs/PollADatabaseEvery60.md: -------------------------------------------------------------------------------- 1 | 2 | 5 | # 6 | 7 | ## Question 8 | 9 | When the user switches to a particular panel, I'd like to start regularly polling my 10 | backend (database) - say every 60 seconds. And, then later, when the user switches 11 | away from that panel, I want to stop that polling. 12 | 13 | ## First, What Not To Do 14 | 15 | Please be sure to read [How Do I Load On Mount?](LoadOnMount.md) 16 | 17 | ## An Answer 18 | 19 | 20 | We'll create an effect. It will be general in nature. 21 | 22 | It will start and stop the timed/scheduled dispatch of an event. 23 | For this FAQ, 24 | we want an event dispatched every 60 seconds and each event will 25 | trigger a backend poll, but the effect we are about to create 26 | will be useful well beyond this narrow case. 27 | 28 | We'll be creating an `effect` called, say, `:interval`. So, event handlers 29 | will be returning: 30 | ```clj 31 | {:interval } 32 | ``` 33 | So now we design the `` bit. It will be a data format (DSL) which 34 | allows an event handler to start and stop a regular event dispatch. 35 | 36 | To `:start` a regular dispatch, an event handler would return 37 | data in this format: 38 | ```clj 39 | {:interval {:action :start 40 | :id :panel-1-query ;; my id for this (so I can cancel later) 41 | :frequency 60000 ;; how many ms between dispatches 42 | :event [:panel-query 1]}} ;; what to dispatch 43 | ``` 44 | 45 | And to later cancel the regular dispatch, an event handler would return this: 46 | ```clj 47 | {:interval {:action :cancel 48 | :id :panel-1-query}} ;; the id provided to :start 49 | ``` 50 | 51 | With that design work done, let's now implement it by registering an 52 | `effect handler`: 53 | ```clj 54 | (re-frame.core/reg-fx ;; the re-frame API for registering effect handlers 55 | :interval ;; the effect id 56 | (let [live-intervals (atom {})] ;; storage for live intervals 57 | (fn [{:keys [action id frequency event]}] ;; the handler 58 | (if (= action :start) 59 | (swap! live-intervals assoc id (js/setInterval #(dispatch event) frequency)) 60 | (do (js/clearInterval (get @live-intervals id)) 61 | (swap! live-intervals dissoc id)))))) 62 | ``` 63 | 64 | You'd probably want a bit more error checking, but that's the (untested) sketch. 65 | 66 | ## A Side Note About Effect Handlers and Figwheel 67 | 68 | [Figwheel](https://github.com/bhauman/lein-figwheel) provides for the hot reloading of code, which 69 | is terrific. 70 | 71 | But, during development, as Figwheel is reloading code, effectful handlers, like the 72 | one above, can be get into a messed up state - existing timers might be lost (and 73 | become never-stoppable). 74 | 75 | Stateful things are grubby in the face of reloading, and all we can do is 76 | try to manage for it as best we can, on a case by case basis. 77 | 78 | One strategy is to put all your grubby effect handlers into their own 79 | separate namespace `effects.cljs` - one that isn't edited often, removing 80 | the trigger for a Figwheel reload. 81 | 82 | OR, you can code defensively for reloading, perhaps like this: 83 | ```clj 84 | (defonce interval-handler ;; notice the use of defonce 85 | (let [live-intervals (atom {})] ;; storage for live intervals 86 | (fn handler [{:keys [action id frequency event]}] ;; the effect handler 87 | (condp = action 88 | :clean (doall ;; <--- new. clean up all existing 89 | (map #(handler {:action :end :id %1}) (keys @live-intervals)) 90 | :start (swap! live-intervals assoc id (js/setInterval #(dispatch event) frequency))) 91 | :end (do (js/clearInterval (get @live-intervals id)) 92 | (swap! live-intervals dissoc id)))) 93 | 94 | ;; when this code is reloaded `:clean` existing intervals 95 | (interval-handler {:action :clean}) 96 | 97 | ;; now register 98 | (re-frame.core/reg-fx ;; the re-frame API for registering effect handlers 99 | :interval ;; the effect id 100 | interval-handler) 101 | ``` 102 | 103 | **Key takeaway:** every effect handler is statefully grubby in its own special way. So you'll have to 104 | come up with strategies to handle Figwheel reloads on a case by case basis. Sometimes 105 | there's no escaping an application restart. 106 | -------------------------------------------------------------------------------- /docs/FAQs/ViewsOnGlobalRegistration.md: -------------------------------------------------------------------------------- 1 | 2 | 5 | # 6 | 7 | ## Question 8 | 9 | I feel offended by re-frame's `reg-*` API. How is it functional to side effect globally? 10 | 11 | ## Background 12 | 13 | A re-frame app is defined collectively by its handlers. As an app boots, calls to registration 14 | functions like `reg-event-db` and `reg-sub` 15 | collectively "build up" an app, infusing it with behaviour and capability. 16 | 17 | Currently, this "building up" process involves the progressive mutation of 18 | a global `registrar` (map) held internally within `re-frame`. 19 | Each registration adds a new entry to this `registrar`. 20 | 21 | How should we analyse this from a functional point of view? 22 | 23 | ## Answer 24 | 25 | There are three ways to view this: 26 | 27 | 1. Egads! Say it isn't true. Mutation of a global? Summon the functional lynch mob! 28 | 29 | 2. In theory, top-level side effects will introduce some pain points, 30 | but re-frame's design represents a conscious decision to trade off functional purity 31 | for simplicity of everyday developer experience. 32 | So, yes, re-frame represents a point in 33 | the possible design space, with associated pros and cons. But the cons tend to be 34 | theoretical and the pros are real. 35 | 36 | 3. Actually, there's no purity problem! As a Clojure program 37 | starts, each `defn` (becomes a `def` which) happily 38 | `interns` a symbol and function in [a map-ish structure](https://clojuredocs.org/clojure.core/ns-interns) representing a `namespace`. 39 | The lynch mob stays home for that. The pitchforks remain in their rack. 40 | `re-frame` handler registration 41 | is the same pattern - an `id` and `handler function` are interned 42 | within a map-ish structure (a `registrar`), once, on program load. 43 | So, if you feel uncomfortable with what re-frame does, you should also feel uncomfortable about using `defn`. 44 | Also, it would be useful to understand 45 | [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) 46 | 47 | 48 | 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 49 | properly until you have used it and experienced the simplicity, and/or found pain points (devcards!). 50 | Many people experience few problems and live happily ever after. For others, the conceptual 51 | distaste is insurmountable and nagging. Like it or hate it, please realise it was a deliberate 52 | and conscious design decision, not some oversight. 53 | -------------------------------------------------------------------------------- /docs/FAQs/When-Does-Dispatch-Happen.md: -------------------------------------------------------------------------------- 1 | 2 | 5 | # 6 | 7 | ### Question 8 | 9 | How long after I call `dispatch` will the event be processed? 10 | 11 | ### Answer 12 | 13 | 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) 14 | might provide you the answers you seek. 15 | -------------------------------------------------------------------------------- /docs/FAQs/Why-CLJC.md: -------------------------------------------------------------------------------- 1 | 2 | 5 | # 6 | 7 | ## Question 8 | 9 | Why is re-frame implemented in `.cljc` files? Aren't ClojureScript 10 | files meant to be `.cljs`? 11 | 12 | ## Answer 13 | 14 | So tests can be run on both the JVM and the JS platforms, 15 | re-frame's implementation is mostly in `.cljc` files. 16 | 17 | The trailing `c` in `.cljc` stands for `common`. 18 | 19 | Necessary interop for each platform can be found in 20 | `interop.clj` (for the JVM) and `interop.cljs` (for JS). 21 | 22 | See also: 23 | -------------------------------------------------------------------------------- /docs/FAQs/Why-Clear-Sub-Cache.md: -------------------------------------------------------------------------------- 1 | 2 | 5 | # 6 | 7 | ## Question 8 | 9 | Why do we call `clear-subscription-cache!` when reloading code with Figwheel? 10 | 11 | ## Answer 12 | 13 | Pour yourself a drink, as this is a circuitous tale involving one of the hardest 14 | problems in Computer Science. 15 | 16 | **1: Humble beginnings** 17 | 18 | When React is rendering, if an exception is thrown, it doesn't catch or 19 | handle the errors gracefully. Instead, all of the React components up to 20 | the root are destroyed. When these components are destroyed, none of their 21 | standard lifecycle methods are called, like `ComponentDidUnmount`. 22 | 23 | 24 | **2: Simple assumptions** 25 | 26 | Reagent tracks the watchers of a Reaction to know when no-one is watching and 27 | it can call the Reaction's `on-dispose`. Part of the book-keeping involved in 28 | this requires running the `on-dispose` in a React `ComponentWillUnmount` lifecycle 29 | method. 30 | 31 | At this point, your spidey senses are probably tingling. 32 | 33 | **3: The hardest problem in CS** 34 | 35 | re-frame subscriptions are created as Reactions. re-frame helpfully deduplicates 36 | subscriptions if multiple parts of the view request the same subscription. This 37 | is a big efficiency boost. When re-frame creates the subscription Reaction, it 38 | sets the `on-dispose` method of that subscription to remove itself from the 39 | subscription cache. This means that when that subscription isn't being watched 40 | by any part of the view, it can be disposed. 41 | 42 | **4: The gnarly implications** 43 | 44 | If you are 45 | 46 | 1. Writing a re-frame app 47 | 2. Write a bug in your subscription code (your one bug for the year) 48 | 3. Which causes an exception to be thrown in your rendering code 49 | 50 | then: 51 | 52 | 1. React will destroy all of the components in your view without calling `ComponentWillUnmount`. 53 | 2. Reagent will not get notified that some subscriptions are not needed anymore. 54 | 3. The subscription on-dispose functions that should have been run, are not. 55 | 4. re-frame's subscription cache will not be invalidated correctly, and the subscription with the bug 56 | is still in the cache. 57 | 58 | At this point you are looking at a blank screen. After debugging, you find the problem and fix it. 59 | You save your code and Figwheel recompiles and reloads the changed code. Figwheel attempts to re-render 60 | from the root. This causes all of the Reagent views to be rendered and to request re-frame subscriptions 61 | if they need them. Because the old buggy subscription is still sitting around in the cache, re-frame 62 | will return that subscription instead of creating a new one based on the fixed code. The only way around 63 | this (once you realise what is going on) is to reload the page. 64 | 65 | **5: Coda** 66 | 67 | re-frame 0.9.0 provides a new function: `re-frame.core/clear-subscription-cache!` which will run the 68 | on-dispose function for every subscription in the cache, emptying the cache, and causing new subscriptions 69 | to be created after reloading. 70 | -------------------------------------------------------------------------------- /docs/FAQs/laggy-input.md: -------------------------------------------------------------------------------- 1 | 2 | 5 | # 6 | 7 | ## Question 8 | 9 | 10 | My input field is laggy. When the user types quickly, it is dropping characters. I have implemented it like this: 11 | 12 | ```clj 13 | [:input 14 | {:type "text" 15 | :value @(rf/subscribe [::subs/username]) 16 | :on-change #(rf/dispatch [::events/change-username (-> % .-target .-value)])}] 17 | ``` 18 | 19 | ## Answer 20 | 21 | That `on-change` handler is being called after the user types every character. If the user is typing very quickly, then the following race condition can occur: 22 | 23 | 1. The user types a new character taking the field from `state A` to `state B` (`B` has one new, extra character in it, compared to `state A`) 24 | 2. The change event for `state B` is dispatched by `on-change`. And that event is queued for processing. 25 | 3. But before that event can be processed, the browser schedules an animation frame. 26 | 4. In that animation frame the component is re-rendered 27 | 5. But during that re-render the `subscribe` will deliver `state A` 28 | 6. That means the text in the box will revert from `state B` to `state A` (the character just typed won't be in the input) 29 | 7. Now if nothing happened till the next animation frame the situation would resolve itself. Because `state B` would be rendered next time because the event which included the 30 | new character would have been processed well before then. 31 | 6. BUT if the user immediately types another character, the state dispatched will be `State A + new character`. The previous character typed, 32 | which caused A -> B, is now lost. 33 | 34 | Bottom line: with very fast typing, characters can get dropped just before animation-frames. 35 | 36 | There are three solutions: 37 | 38 | 1. don't use `on-change`, and instead use `on-blur` which is only called when the user has done all their fast typing and they leave the field. 39 | 2. if you have to use `on-change` then switch to use `dispatch-sync` in `on-change`, instead of `dispatch`. The event will not be placed on the queue. It will be handled immediately. Synchronously. 40 | 3. use a component from something like re-com because it has been engineered to not have this problem. Or copy the (local state) technique it uses. 41 | 42 | -------------------------------------------------------------------------------- /docs/FAQs/use-cofx-as-fx.md: -------------------------------------------------------------------------------- 1 | 4 | # 5 | 6 | ## Question 7 | 8 | With `reg-event-fx`, why can't my handler function just update the first argument (i.e. the `cofx` map) and pass it on? 9 | 10 | When I try, I get a warning, such as: `no handler registered for effect: :event. Ignoring.` 11 | 12 | ## Answer 13 | 14 | Effects simply aren't coeffects. 15 | 16 | To fix this warning, just declare a new map of effects. For instance: 17 | 18 | ```clj 19 | (reg-event-fx ::cow-clicked 20 | (fn [{:keys [db] :as cofx} _] 21 | {:db (update db :clicks inc)})) 22 | ``` 23 | 24 | ## Context 25 | 26 | This seems like it would work, if you're used to using `reg-event-db` this way. 27 | Such a `db` handler behaves like a reducing function: 28 | 29 | ```clj 30 | (reg-event-db ::cow-clicked-db 31 | (fn [db _] (update db :clicks inc))) 32 | ``` 33 | 34 | Doing a similar thing to a coeffect map causes the warning, however, not to mention possible unintended effects: 35 | 36 | ```clj 37 | (reg-event-fx ::cow-clicked-bad 38 | (fn [cofx _] (update-in cofx [:db :clicks] inc))) 39 | ``` 40 | 41 | With `reg-event-fx`, you receive a map of coeffect values, and you're expected to return a map of effect values. 42 | It's a coincidence that `:db` identifies both an effect *and* a coeffect. 43 | But re-frame adds other coeffects, such as `:event`, which it does *not* consider effects. 44 | 45 | So your handler ends up doing `(update-in {:db {:clicks 0} :event [::cow-clicked-bad] :your-other-coeffect 25} ...)`. 46 | 47 | There's probably no effect handler for `:event`. Hence the warning. 48 | Coeffects which you declare, such as `your-other-coeffect`, may cause the same problem. 49 | 50 | You might have a coeffect, similar to `:db`, which shares its name with an effect. But that's a deliberate design decision, not a default. 51 | -------------------------------------------------------------------------------- /docs/Figma Infographics/inforgraphics.fig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/day8/re-frame/7b6fa64a6b3aecd2fcdbbfd80a89d59f92de57e1/docs/Figma Infographics/inforgraphics.fig -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ## Introduction 2 | 3 | This directory contains both the content and configuration for the re-frame website. 4 | 5 | The live, production copy of the re-frame website can be viewed here: 6 | https://day8.github.io/re-frame/ 7 | 8 | This document describes how the website is built and how to do work on it. 9 | 10 | 11 | ## Built Using 12 | 13 | The re-frame website is largly built using a static site generator: 14 | which provides 15 | a [Material UI](https://material.io/) theme for the `mkdocs` static site generator. 16 | 17 | The live re-frame website is built [via Github actions](https://github.com/day8/re-frame/blob/feature/mkdocs/.github/workflows/docs-workflow.yml) 18 | which stitch together the docs, the API and the docs app. 19 | 20 | ## To Build Locally 21 | 22 | For development purposes, you can get a hot reloading docs environment going via `docker` by using the following series of commands: 23 | ```sh 24 | git clone https://github.com/day8/re-frame.git 25 | cd re-frame 26 | ``` 27 | 28 | Then build the API documentation (generally, just once): 29 | ```sh 30 | cd docs/ 31 | clojure -m ns-to-markdown ../src/re_frame/core.cljc > api-re-frame.core.md 32 | ``` 33 | 34 | Then switch back to the re-frame home directory to generate the rest of the docs. 35 | ```sh 36 | cd .. 37 | ``` 38 | 39 | If using PowerShell on Windows (and I'm told this is also the right commandline for Mac): 40 | ```sh 41 | docker run --rm -it -p 8000:8000 -v "%cd%":/docs squidfunk/mkdocs-material:5.1.1 42 | ``` 43 | or, if you are using linux: 44 | ```sh 45 | docker run --rm -it -p 8000:8000 -v ${PWD}:/docs squidfunk/mkdocs-material:5.1.1 46 | ``` 47 | 48 | Then, in your browser tab, load `http://localhost:8000/`. You should see the website Home page. 49 | 50 | You can now edit the markdown in `/docs` and your changes will be hot reloaded into the browser tab for inspection. 51 | 52 | 53 | ## Configuration 54 | 55 | The main configuration file for the static site generator is: 56 | `../mkdocs.yml` 57 | 58 | In that file you can nominate navigation, fonts, extensions, etc. 59 | 60 | ## Look And Feel Adjustments 61 | 62 | We've made various modifications to the base template supplied by the theme ... 63 | 64 | Notably adds 65 | - Google Font stylesheets 66 | - our own stylesheets for overriding some styles 67 | 68 | `overrides/partials/footer.html` 69 | Removes 'Made with Material for MkDocs' from footer. 70 | Attribution in documentation, not footer. 71 | 72 | `overrides/partials/nav.html` 73 | Adds logo above left nav. (Deprecated?) 74 | 75 | `overrides/main.html` 76 | htmltitle block 77 | Removes trailing dash in page title due to empty site name. 78 | 79 | `stylesheets/re-frame.css` 80 | Our own custom styles. 81 | -------------------------------------------------------------------------------- /docs/The-re-frame-logo.md: -------------------------------------------------------------------------------- 1 |

re-frame logo

2 | # 3 | ## Who? 4 | 5 | It was created by a mysterious, deep thinker, known only as @martinklepsch. 6 | 7 | Some say he appears on high-value stamps in Germany and that he once 8 | knocked a horse unconscious with a single punch. Others say he loves recursion 9 | so much that, in his wallet, he carries a photograph of his wallet. 10 | 11 | All we know for sure is that he wields pixels like Bruce Lee wielded 12 | nunchucks. 13 | 14 | ## Genesis Theories 15 | 16 | Great, unexplained works encourage fan theories, and the re-frame 17 | logo is no exception. 18 | 19 | One noisy group insist that @martinklepsch's design is assertively 20 | putting the 'f' back into infinity, in a sicking-it-to-the-man kinda way. 21 | They have t-shirts, `f u ∞`, and angry certainty. 22 | 23 | Another group speculates that he created the logo as a bifarious rainbow 24 | homage to Frank Lloyd Wright's architectural masterpiece, the Guggenheim 25 | Museum. Which is surely a classic case of premature abstraction and 26 | over-engineering. Their theory, not the Guggenheim. They sometimes 27 | indent by 5 spaces - because "art" - and need ultrawide monitors 28 | for yaml. 29 | 30 | ![](images/logo/Guggenheim.jpg) 31 | 32 | Then there's the infamous "Bad Touch" faction. So embarrassing. For them, 33 | the logo shows the ClojureScript logo mating noisily with re-frame's 34 | official architecture diagram. Colour-wise, I guess. Their parties are 35 | completely awesome, but you might need someone to bail you out of 36 | jail later. 37 | 38 | ![](images/logo/Genesis.png) 39 | 40 | For the Functional Fundamentalists, an earnest bunch with post-Maharishi-Beatles 41 | hair, the logo is a flowing poststructuralist rebuttal of OO's vowel duplication 42 | and horizontal adjacency. Their alternative abbreviation, FF, is fine, apparently, 43 | because "hey, come on man, everyone loves a fricative". 44 | 45 | For his part, @martinklepsch has never confirmed any theory, teasing 46 | us instead with coded clues like "Will you please stop emailing me" 47 | and "Why did you say I hit a horse?". 48 | 49 | -------------------------------------------------------------------------------- /docs/all-models-are-wrong.md: -------------------------------------------------------------------------------- 1 | # Mental Model Omnibus 2 | 3 | 4 | 5 | > All models are wrong, but some are useful 6 | 7 | The re-frame tutorials initially focus on the domino 8 | narrative. The goal is to efficiently explain the mechanics of re-frame, 9 | and to get you reading and writing code ASAP. 10 | 11 | But **there are other perspectives** on re-frame 12 | which will deepen your understanding. 13 | 14 | This section of the tutorial is a tour of these ideas. 15 | 16 | > If a factory is torn down but the rationality which produced it is 17 | left standing, then that rationality will simply produce another 18 | factory. If a revolution destroys a government, but the systematic 19 | patterns of thought that produced that government are left intact, 20 | then those patterns will repeat themselves.
21 | > -- Robert Pirsig, Zen and the Art of Motorcycle Maintenance 22 | 23 | -------------------------------------------------------------------------------- /docs/api-intro.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | The re-frame API consists of: 4 | 5 | - the namespace `re-frame.core` 6 | - a set of built-in effects 7 | 8 | In the navigation to the left, you'll see a link to both. 9 | 10 | ## Dependency Information 11 | 12 | Please review both the [releases page](http://day8.github.io/re-frame/releases/2022) and the [Clojars page](https://clojars.org/re-frame/) to discover the version you should be using. 13 | 14 | 15 | ## Using re-frame 16 | 17 | To use the re-frame API within your namespace, you'll 18 | need to `require` it, perhaps like this: 19 | ```clj 20 | (ns my-app.some-namespace 21 | (:require [re-frame.core :as rf])) 22 | 23 | ;; your code here 24 | ``` 25 | 26 | You'll then be able to use the functions in the API, perhaps like this: `#!clj rf/dispatch`. 27 | 28 | 29 | ## The Most Commonly Used Part Of The API 30 | 31 | When you are writing `View Functions`: 32 | 33 | - `dispatch` (or occasionally, `dispatch-sync`) 34 | - `subscribe` 35 | 36 | When you are registering: 37 | 38 | - event handlers - `reg-event-db` and `reg-event-fx` 39 | - subscription handlers - `reg-sub` (and rarely `reg-sub-raw`) 40 | - rarely, effect handlers - `reg-fx` 41 | - rarely, coeffect handlers - `reg-cofx` with `inject-cofx` 42 | 43 | When you register `event handlers`, you might use builtin interceptors: 44 | 45 | - `path` 46 | - `on-change` 47 | - `enrich` 48 | - `after` 49 | - `trim-v` 50 | - `debug` 51 | 52 | Global interceptors can be very useful: 53 | 54 | - register them via `reg-global-interceptors` 55 | - rarely, remove them via `clear-global-interceptor` 56 | 57 | When errors arise: 58 | 59 | - Catch them from events and interceptors via `reg-event-error-handler` 60 | 61 | ## More Rarely Used Part 62 | 63 | Testing or dev-time related utilities: 64 | 65 | - `clear-subscription-cache!` 66 | - `make-restore-fn` 67 | - `purge-event-queue` 68 | 69 | Logging/debugging: 70 | 71 | - `console` 72 | - `set-loggers` 73 | 74 | 75 | If you write an Interceptor, use these utilities. To see how they are used, look 76 | at the [re-frame code for builtin Interceptors](https://github.com/day8/re-frame/blob/master/src/re_frame/std_interceptors.cljc): 77 | 78 | - `->interceptor` 79 | - `get-coeffect` 80 | - `assoc-coeffect` 81 | - `get-effect` 82 | - `assoc-effect` 83 | - `enqueue` 84 | -------------------------------------------------------------------------------- /docs/breaking-it.md: -------------------------------------------------------------------------------- 1 | > Document is WIP 2 |
3 | 4 | > To understand how something works, figure out how to break it 5 | > -- N.N.Talib 6 | 7 | All libraries/frameworks should come with a "what breaks this" section - this is that document for re-frame. 8 | 9 | The essence of science is earnestness of inquiry. 10 | 11 | > Eddington defined science as “the earnest endeavour to put into order the facts of experience” 12 | 13 | So what are we doing here? Marketing (Narrative warefare) for the re-frame framework or computer science. 14 | 15 | ## Pros 16 | 17 | - is very productive 18 | - has simple dynamics (#1 importance) 19 | - you write your app in less lines of code (#2 importance) 20 | - sits in a sea of tranquility, compared to the technical churn elsewhere 21 | - best in breed hot reloading process, because of tooling and pervasive immutable data 22 | 23 | - it leverages 24 | - pure functions 25 | - immutable data 26 | - declarative style, using data-based DSLs 27 | - a shockingly effective and beautiful language (50 years of refinement by the finest minds in computer science) 28 | - React's entire ecosystem of components - although Hooks is starting to bimodalate (?word) the ecosystem. 29 | - full interop with js 30 | - reactive data flows 31 | - a data oriented design 32 | - excellent tooling 33 | - shadow-clj and figwheel 34 | - clj-devtools 35 | - re-frame-10x 36 | - Google's "Closure Compiler" (tree shaking) 37 | 38 | Also benefits from: 39 | 40 | - acceptable performance 41 | - acceptable bundle size 42 | - is mature 43 | - is easily learned (sometimes Clojure itself can be an initial hurdle, if you don't know functional programming) 44 | - an enthusiastic community, video training and 3rd party libraries 45 | 46 | 47 | - re-frame-10x is only half finished. It is entirely useful and functional, but I'd love to take it the rest of the way. 48 | - is functional and has a data oriented design (has unique features) 49 | 50 | It is Boringly simple 51 | 52 | 53 | 54 | ## Cons 55 | 56 | 57 | As the framework author, I should be a relentless chearleader, right?. The gyrations of my pompoms should be tecktonic. 58 | 59 | But one of the best ways for me to help you, an evaluator of this framework, is explain where and how it doesn't work well? That's this section. 60 | 61 | Every design represents a point in design space, with pros and cons. The tradeoffs are the interesting bit. 62 | 63 | So I'll do that now. 64 | 65 | 66 | I will not try to contrast re-frame with your other framework options. I wish I could provide you with 67 | that, but to do that well, i would need deep knowledge of all the frameworks, and I don't know of 68 | anyone who really has that. Certainly not me. I try to keep an eye on them, but by "deep knowledge" 69 | I mean you need to have used it in anger, professionally for a couple of years. Anything less and 70 | the comparisons tend to be too shallow and misleading - which means: not unsful - particularly when they 71 | are written by someone with a 72 | 73 | 74 | 75 | Rather than telling you what's awesome, I should take Talib's advice and tell you what breaks re-frame. What doesn't work. In that way you will know it better than any amount of "its so awesome". 76 | 77 | What breaks re-frame: 78 | 79 | - too many events - maybe a telemetary app? 80 | - if you want to use components which use React hooks 81 | - it might not be a good fit if your app is a very thin venier over a remote database, and forms dominate the process. re-frame can do forms, but it is probably better when the UI get's more complex. 82 | - I'm not sure the testing story is as strong as it could be. Mind you, it seems good enough that I haven't been tempted to improve it. 83 | - A lot of chat with a server HTTP results in too many (we plan to fix this, but right now XXXX) 84 | - Server side rendering. Is possible if you use node, but other platforms, maybe not. 85 | - We'd like a better FSM story 86 | - framework vs library. 87 | - dynamically typed 88 | 89 | 90 | 91 | A Framework should be invisible and boring. So, where you notice it ... that's an example of it being broken. Connection with server?? 92 | 93 | 94 | XXX I will say that I don't think React is on the right track with hooks, Suspense. React was at its best when it tried to be the `V` in `MVC`. 95 | -------------------------------------------------------------------------------- /docs/browser-dynamics.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | The re-frame loop plays out, over time, within the browser. And browsers are bursty, noisy things. 4 | 5 | Here's an infographic describing the ideal process. 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/cljdoc.edn: -------------------------------------------------------------------------------- 1 | {:cljdoc.doc/tree [["README" {:file "README.md"}]] 2 | :cljdoc.api/namespaces []} 3 | -------------------------------------------------------------------------------- /docs/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | 3 | :deps {org.clojure/clojure {:mvn/version "1.10.1"} 4 | org.clojure/clojurescript {:mvn/version "1.10.773"}}} 5 | -------------------------------------------------------------------------------- /docs/event-handling-infographic.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 |
8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /docs/images/404-img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/day8/re-frame/7b6fa64a6b3aecd2fcdbbfd80a89d59f92de57e1/docs/images/404-img.png -------------------------------------------------------------------------------- /docs/images/Alien3_0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/day8/re-frame/7b6fa64a6b3aecd2fcdbbfd80a89d59f92de57e1/docs/images/Alien3_0.jpg -------------------------------------------------------------------------------- /docs/images/Readme/6dominoes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/day8/re-frame/7b6fa64a6b3aecd2fcdbbfd80a89d59f92de57e1/docs/images/Readme/6dominoes.png -------------------------------------------------------------------------------- /docs/images/Readme/Dominoes-small.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/day8/re-frame/7b6fa64a6b3aecd2fcdbbfd80a89d59f92de57e1/docs/images/Readme/Dominoes-small.jpg -------------------------------------------------------------------------------- /docs/images/Readme/Dominoes.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/day8/re-frame/7b6fa64a6b3aecd2fcdbbfd80a89d59f92de57e1/docs/images/Readme/Dominoes.jpg -------------------------------------------------------------------------------- /docs/images/Readme/todolist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/day8/re-frame/7b6fa64a6b3aecd2fcdbbfd80a89d59f92de57e1/docs/images/Readme/todolist.png -------------------------------------------------------------------------------- /docs/images/delete.me.event-handlers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/day8/re-frame/7b6fa64a6b3aecd2fcdbbfd80a89d59f92de57e1/docs/images/delete.me.event-handlers.png -------------------------------------------------------------------------------- /docs/images/epoch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/day8/re-frame/7b6fa64a6b3aecd2fcdbbfd80a89d59f92de57e1/docs/images/epoch.png -------------------------------------------------------------------------------- /docs/images/event-dispatch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/day8/re-frame/7b6fa64a6b3aecd2fcdbbfd80a89d59f92de57e1/docs/images/event-dispatch.png -------------------------------------------------------------------------------- /docs/images/example_app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/day8/re-frame/7b6fa64a6b3aecd2fcdbbfd80a89d59f92de57e1/docs/images/example_app.png -------------------------------------------------------------------------------- /docs/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/day8/re-frame/7b6fa64a6b3aecd2fcdbbfd80a89d59f92de57e1/docs/images/favicon.png -------------------------------------------------------------------------------- /docs/images/handling-one-event.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/day8/re-frame/7b6fa64a6b3aecd2fcdbbfd80a89d59f92de57e1/docs/images/handling-one-event.png -------------------------------------------------------------------------------- /docs/images/interceptors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/day8/re-frame/7b6fa64a6b3aecd2fcdbbfd80a89d59f92de57e1/docs/images/interceptors.png -------------------------------------------------------------------------------- /docs/images/logo/Genesis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/day8/re-frame/7b6fa64a6b3aecd2fcdbbfd80a89d59f92de57e1/docs/images/logo/Genesis.png -------------------------------------------------------------------------------- /docs/images/logo/Guggenheim.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/day8/re-frame/7b6fa64a6b3aecd2fcdbbfd80a89d59f92de57e1/docs/images/logo/Guggenheim.jpg -------------------------------------------------------------------------------- /docs/images/logo/README.md: -------------------------------------------------------------------------------- 1 | 2 | ![logo](re-frame-colour.png?raw=true) 3 | 4 | See the `ai` folder to illustrator files. 5 | 6 | -------------------------------------------------------------------------------- /docs/images/logo/ai/Re-frame colour.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/day8/re-frame/7b6fa64a6b3aecd2fcdbbfd80a89d59f92de57e1/docs/images/logo/ai/Re-frame colour.ai -------------------------------------------------------------------------------- /docs/images/logo/ai/Re-frame white.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/day8/re-frame/7b6fa64a6b3aecd2fcdbbfd80a89d59f92de57e1/docs/images/logo/ai/Re-frame white.ai -------------------------------------------------------------------------------- /docs/images/logo/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/day8/re-frame/7b6fa64a6b3aecd2fcdbbfd80a89d59f92de57e1/docs/images/logo/favicon.ico -------------------------------------------------------------------------------- /docs/images/logo/old/f_303w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/day8/re-frame/7b6fa64a6b3aecd2fcdbbfd80a89d59f92de57e1/docs/images/logo/old/f_303w.png -------------------------------------------------------------------------------- /docs/images/logo/old/frame_1024w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/day8/re-frame/7b6fa64a6b3aecd2fcdbbfd80a89d59f92de57e1/docs/images/logo/old/frame_1024w.png -------------------------------------------------------------------------------- /docs/images/logo/old/re-frame-logo.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/day8/re-frame/7b6fa64a6b3aecd2fcdbbfd80a89d59f92de57e1/docs/images/logo/old/re-frame-logo.sketch -------------------------------------------------------------------------------- /docs/images/logo/old/re-frame_128w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/day8/re-frame/7b6fa64a6b3aecd2fcdbbfd80a89d59f92de57e1/docs/images/logo/old/re-frame_128w.png -------------------------------------------------------------------------------- /docs/images/logo/old/re-frame_256w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/day8/re-frame/7b6fa64a6b3aecd2fcdbbfd80a89d59f92de57e1/docs/images/logo/old/re-frame_256w.png -------------------------------------------------------------------------------- /docs/images/logo/old/re-frame_512w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/day8/re-frame/7b6fa64a6b3aecd2fcdbbfd80a89d59f92de57e1/docs/images/logo/old/re-frame_512w.png -------------------------------------------------------------------------------- /docs/images/logo/re-frame-colour.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/day8/re-frame/7b6fa64a6b3aecd2fcdbbfd80a89d59f92de57e1/docs/images/logo/re-frame-colour.png -------------------------------------------------------------------------------- /docs/images/logo/re-frame-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/day8/re-frame/7b6fa64a6b3aecd2fcdbbfd80a89d59f92de57e1/docs/images/logo/re-frame-white.png -------------------------------------------------------------------------------- /docs/images/mental-model-omnibus.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/day8/re-frame/7b6fa64a6b3aecd2fcdbbfd80a89d59f92de57e1/docs/images/mental-model-omnibus.jpg -------------------------------------------------------------------------------- /docs/images/not-a-domino-lineup.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/day8/re-frame/7b6fa64a6b3aecd2fcdbbfd80a89d59f92de57e1/docs/images/not-a-domino-lineup.jpg -------------------------------------------------------------------------------- /docs/images/not-a-domino.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/day8/re-frame/7b6fa64a6b3aecd2fcdbbfd80a89d59f92de57e1/docs/images/not-a-domino.jpg -------------------------------------------------------------------------------- /docs/images/scale-changes-everything.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/day8/re-frame/7b6fa64a6b3aecd2fcdbbfd80a89d59f92de57e1/docs/images/scale-changes-everything.jpg -------------------------------------------------------------------------------- /docs/images/subscriptions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/day8/re-frame/7b6fa64a6b3aecd2fcdbbfd80a89d59f92de57e1/docs/images/subscriptions.png -------------------------------------------------------------------------------- /docs/images/the-water-cycle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/day8/re-frame/7b6fa64a6b3aecd2fcdbbfd80a89d59f92de57e1/docs/images/the-water-cycle.png -------------------------------------------------------------------------------- /docs/images/yinyang.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/day8/re-frame/7b6fa64a6b3aecd2fcdbbfd80a89d59f92de57e1/docs/images/yinyang.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | template: overrides/home.html 3 | title: Derived data, flowing 4 | --- 5 | -------------------------------------------------------------------------------- /docs/interconnections.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | Ask a Systems Theorist, and they'll tell you that a system has **parts** and **interconnections**. 4 | 5 | Human brains tend to focus first on the **parts**, and then, later, maybe on 6 | **interconnections**. But we know better, right? We 7 | know interconnections are often critical to a system. 8 | "Focus on the lines between the boxes" we lecture anyone kind enough to listen (in my case, glassy-eyed family members). 9 | 10 | In the case of re-frame, dominoes are the **parts**, so, tick, yes, we have 11 | looked at them first. Our brains are happy. But what about the **interconnections**? 12 | 13 | If the **parts** are functions, as is the case with re-frame, 14 | what does it even mean to talk about **interconnections between functions?** 15 | To answer that question, I'll rephrase it as: 16 | how are the domino functions **composed**? 17 | 18 | At the language level, 19 | Uncle Alonzo and Uncle John tell us how a function like `count` composes: 20 | ```clj 21 | (str (count (filter odd? [1 2 3 4 5]))) 22 | ``` 23 | We know when `count` is called, and with what 24 | argument, and how the value it computes becomes the arg for a further function. 25 | We know how data "flows" into and out of the functions. 26 | 27 | Sometimes, we'd rewrite this code as: 28 | ```clj 29 | (->> [1 2 3 4 5] 30 | (filter odd?) 31 | count 32 | str) 33 | ``` 34 | With this arrangement, we talk of "threading" data 35 | through functions. **It seems to help our comprehension to conceive function 36 | composition in terms of data flow**. 37 | 38 | re-frame delivers architecture 39 | by supplying the interconnections - it threads the data - it composes the dominoes - it is the lines between the boxes. 40 | 41 | But it doesn't have a universal method for this "composition". The technique it uses varies from one domino 42 | neighbour-pair to the next. Initially, it uses a queue/router, then a pipeline of interceptors 43 | and, finally, a Signal Graph. 44 | 45 | Remember back in the Introduction? Our analogy for re-frame was the water cycle - water flowing around the loop, 46 | compelled by different kinds of forces at different times (gravity, convection, etc), going through phase changes. 47 | 48 | With this focus on interconnections, we have been looking on the "forces" part of the loop. The transport. 49 | -------------------------------------------------------------------------------- /docs/re-frame.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |

The re-frame Logo

4 | 5 | A framework for building Modern Web Apps in ClojureScript. It leverages React, via [Reagent]. 6 | 7 | McCoy might report "It's MVC, Jim, but not as we know it". And you would respond 8 | "McCoy, you trouble maker, why even mention an OO pattern? 9 | re-frame is a **functional framework**." 10 | 11 | So, it is about `data`, and the `functions` 12 | which transform that data. And, because it is a **reactive framework**, `data` coordinates 13 | `functions`, not the other way around. 14 | 15 | 21 | 22 | 23 | [Reagent]:http://reagent-project.github.io/ 24 | 25 | ## Why Should You Care? 26 | 27 | Perhaps: 28 | 29 | 1. You want to develop a modern web application using ClojureScript. 30 | 2. You want to maximise developer productivity by writing [fewer lines of code](https://medium.com/dailyjs/a-realworld-comparison-of-front-end-frameworks-2020-4e50655fe4c1). You want a simple dynamic process that you can simulate in your head. And you want a clean approach to effects and state management. 31 | 2. You are curious about the benefits of **_data-oriented design_**. 32 | 2. You are a refugee from technical churn, seeking stability and productivity. 33 | For six years, ClojureScript, Reagent and re-frame have barely changed. No need. Still cutting edge. 34 | 2. You want to see how `reactive programming`, `functional programming` and `immutable data` 35 | combine in a language that genuinely embraces those paradigms. 36 | 3. You're taking a [Functional Design and Programming course](http://www.eli.sdsu.edu/courses/fall15/cs696/index.html) at San Diego State University 37 | and you have a re-frame assignment due. You've left the reading a bit late, right? 38 | 4. You seek a better Redux, Elm, Cycle.js or Pux. In this space, re-frame is very old, 39 | hopefully in a Gandalf kind of way. 40 | Designed in late 2014, it slightly pre-dates the official Elm Architecture, 41 | although thankfully we picked up `foldp` ideas from early Elm games. 42 | Our main inspiration was the 43 | Clojure projects [Pedestal App], [Hoplon] and [Om]. Since then, 44 | re-frame has pioneered ideas like event handler middleware, 45 | coeffect accretion, and de-duplicated signal graphs. 46 | 5. Which brings us to the most important point: **re-frame is impressively buzzword compliant**. It has reactivity, 47 | unidirectional data flow, pristinely pure functions, 48 | interceptors, coeffects, conveyor belts, algebraic effects, statechart-friendliness 49 | and claims an immaculate hammock conception. All while being both simple and easy. There's also a charming 50 | xkcd reference (soon) and a hilarious, insiders-joke T-shirt, 51 | ideal for conferences (in design). 52 | 53 | What could possibly go wrong? 54 | 55 | [Pedestal App]:https://github.com/pedestal/pedestal-app 56 | [SPA]:http://en.wikipedia.org/wiki/Single-page_application 57 | [OM]:https://github.com/swannodette/om 58 | [Hoplon]:http://hoplon.io/ 59 | 60 | 61 | 62 | ## It Is Mature 63 | 64 | re-frame was released in early 2015, and has since 65 | [been](https://www.fullcontact.com) successfully 66 | [used](https://www.nubank.com.br) by 67 | [many](http://open.mediaexpress.reuters.com/) a 68 | [companies](https://rokt.com/) and 69 | individuals to build complex apps, many running beyond 40K lines of 70 | ClojureScript. 71 | 72 | 73 | 74 | **Scale changes everything.** Frameworks 75 | are just pesky overhead at small scale - measure them instead by how they help 76 | you tame the complexity of bigger apps, and in this regard re-frame has 77 | worked out well. Some have been effusive in their praise. 78 | 79 | And, yes, re-frame is fast, straight out of the box. And, yes, it has 80 | a good testing story (unit and behavioural). And, yes, it works with 81 | tools like figwheel or shadow-cljs to create 82 | a powerful hot-loading development story. And, yes, it has 83 | fun specialist tooling, and a community, 84 | and useful 3rd party libraries. 85 | 86 | 87 | 88 | 96 | # 97 | -------------------------------------------------------------------------------- /docs/reagent.md: -------------------------------------------------------------------------------- 1 | 2 | Clojurescript is ergonomic, stable, functional language 3 | 4 | Look at the front page of https://www.learnreframe.com/ 5 | 6 | 7 |
8 | (ns simple.core 9 | (:require [reagent.dom.client :as rdc] 10 | [re-frame.core :as rf] 11 | [clojure.string :as str])) 12 |
13 | 14 |
15 | (defn square 16 | [] 17 | [:svg {:width "400" :height "110"} 18 | [:rect {:width "300" :height "100"}]]) 19 |
20 | 21 |
22 | [square] 23 |
24 | 25 | 26 | 27 |
28 | (defonce root-container 29 | (rdc/create-root (js/document.getElementById "live-app"))) 30 | 31 | (defn mount-ui 32 | [] 33 | (rdc/render root-container [square])) ;; mount the application's ui 34 | 35 | (mount-ui) 36 |
37 | 38 | 39 | 40 |
41 |
42 | The live application should start here in 60 seconds ... 43 |

44 | Doesn't work? Maybe try disabling your adblocker for this site. 45 | 46 | 47 |
48 |
49 | 50 | 51 | ``` 52 | (defn square 53 | [colour size] 54 | [:div {:style 55 | {:width (str (or size "20") "px") 56 | :height (str (or size "20") "px") 57 | :display "inline-block" 58 | :background-color colour}}]) 59 | 60 | [:div [square "blue" 40] [square "green"]] 61 | ``` 62 | 63 | XXX work from 64 | 65 | ## Atoms 66 | 67 | Atoms are a clojure language concept. They provide a way to store a mutable value - a value which can change over time. This value in an atom might be an int or a map ... anything. 68 | 69 | You used the function `deref` to o obtain the value held in an atom. So if `a` is an atom, 70 | then `(deref a)` will extrct the value. The shorthand is `@a`. 71 | 72 | You can set the value of an atom with `reset!`, like this `(reset! a :foo)`. 73 | 74 | Alternatively, you canuse `swap!` to update the value in an atom. `swap!` takes a pure function as an argument which is to perform this `update`. It is expected to takes the current value in the atom as an argument, and to return the updated value to be put back in the atom. 75 | 76 | For example `(swap! a + 5 6)` will call the function `+` with three values: the value in `a` and `5` and `6`. So this will effectively add 11 to the value currently in `a` and store that value back into `a`. 77 | -------------------------------------------------------------------------------- /docs/releases/2017.md: -------------------------------------------------------------------------------- 1 | 2 | 5 | # 6 | 7 | ## 0.10.2 (2017.10.07) 8 | 9 | #### New Features 10 | 11 | - On dispose callbacks now run on JVM re-frame. [#386](https://github.com/day8/re-frame/pull/386). 12 | - Log warnings when tracing is not enabled and you try to add a trace callback [#395](https://github.com/day8/re-frame/pull/395). 13 | 14 | #### New Docs 15 | 16 | - added [a new FAQ entry](/docs/FAQs/PollADatabaseEvery60.md) How do I turn on/off polling a database every 60 secs (hint: effects) 17 | - added [a new FAQ entry](/docs/FAQs/FullStackReframe.md) How do I do full-stack re-frame? 18 | - Added a gitbook for re-frame docs [#414](https://github.com/day8/re-frame/pull/414). 19 | - Lots of spelling fixes, for which we are eternally grateful. 20 | 21 | ## 0.10.1 (2017.08.17) 22 | 23 | - fix a showstopper bug introduced at the last minute in v0.10.0 24 | 25 | --- 26 | 27 | ## 0.10.0 (2017.08.16) 28 | 29 | #### New Docs 30 | 31 | - added [API documentation](/docs/API.md) 32 | - added [testing docs](/docs/Testing.md) 33 | - added [a new mental model](/docs/MentalModelOmnibus.md#on-dsls-and-machines) 34 | - added [a new FAQ entry](/docs/FAQs/When-Does-Dispatch-Happen.md) on dispatch processing 35 | - added [a new FAQ entry](/docs/FAQs/DB_Normalisation.md) on representing normalised data in `app-db` 36 | - added [a new FAQ entry](/docs/FAQs/GlobalInterceptors.md) on how to register a global interceptor 37 | 38 | #### Breaking 39 | 40 | - [#357](https://github.com/day8/re-frame/pull/357) 41 | I'd be amazed if this actually broke any apps. Shocked! But, better safe than sorry. 42 | The effect handler for `:db` has changed: if the new value provided tests 43 | `identical?` to the existing value within `app-db`, then `app-db` is not `reset!`. 44 | Previously, `app-db` was always `reset!` irrespective, 45 | which potentially caused Layer 2 subscriptions to run unnecessarily. So this is a tiny 46 | efficiency change in this edge case, and it results in behaviour that better matches 47 | programmer intuitions. 48 | 49 | #### Minor Fixes and Improvements 50 | 51 | - [#400](https://github.com/day8/re-frame/pull/400) Improve error message when a registered cofx can't be found 52 | - The effect handler for `:dispatch-n` will now ignore `nils`. [See checkin](https://github.com/day8/re-frame/commit/6efdae438f393f8121a2d6dfbf76db00e6dafbf5) 53 | - [#340](https://github.com/day8/re-frame/pull/340) 54 | - [#341](https://github.com/day8/re-frame/pull/341) Fix `re-frame.core/on-changes` to work even if event handler does not set `:db`. 55 | - [#395](https://github.com/day8/re-frame/pull/395) Warn users if they try to enable tracing without setting the closure define `re-frame.trace.trace-enabled?`. 56 | 57 | 58 | --- 59 | 60 | ## 0.9.4 (2017.06.01) 61 | 62 | #### Improvements 63 | - added a CITATION.md file 64 | - re-frame now supports self-hosted ClojureScript at an alpha/unofficial/experimental level. It may be removed in the future if it causes problems elsewhere. [#325](https://github.com/day8/re-frame/pull/325) 65 | 66 | --- 67 | 68 | ## 0.9.3 (2017.05.15) 69 | 70 | #### Breaking (previously undefined behaviour) 71 | 72 | - `reg-sub` enforces using `:<-` to indicate subscription inputs. Previously any keyword would have worked here. While using anything other than `:<-` was undefined behaviour previously, this could possibly break some code when upgrading. Thanks to [@Sohalt](https://github.com/Sohalt) [#336](https://github.com/day8/re-frame/pull/336). 73 | 74 | #### Fixes 75 | 76 | - `re-frame.interceptor/update-coeffect` has been fixed. [#328](https://github.com/day8/re-frame/pull/328) 77 | - Fix ns form in `re-frame.interceptor`. Thanks to [@ggeoffrey](https://github.com/ggeoffrey). [#338](https://github.com/day8/re-frame/pull/338) 78 | - Even more spelling fixes. 79 | 80 | --- 81 | 82 | ## 0.9.2 (2017.02.09) 83 | 84 | #### Improvements 85 | 86 | - Update dependency on reagent from `0.6.0-rc` to `0.6.0`. 87 | - A truckload of tweaks, spelling fixes, and general improvements to the docs. A big thanks to everyone who contributed! 88 | 89 | #### Fixes 90 | 91 | - Use `:devDependencies` instead of `:dependencies` for the lein-npm Karma dependencies. This stops consumers of re-frame with the lein-npm plugin from having to install Karma and friends. 92 | - Correct `:id` of on-changes interceptor from `:enrich` to `:on-changes` 93 | -------------------------------------------------------------------------------- /docs/releases/2018.md: -------------------------------------------------------------------------------- 1 | 2 | 5 | # 6 | 7 | ## 0.10.6 (2018-09-03) 8 | 9 | #### Fixed 10 | 11 | - After interceptor now runs against effect db if effect db is nil/false. [#447](https://github.com/day8/re-frame/issues/447) 12 | - The effect handler for `:dispatch-later` will now ignore `nils`. [#455](https://github.com/day8/re-frame/issues/455) 13 | - Add locking to the EventQueue when running under JVM to avoid race conditions and subtle errors when dispatching lots of events. [#471](https://github.com/day8/re-frame/pull/471) 14 | 15 | --- 16 | 17 | ## 0.10.5 (2018.02.13) 18 | 19 | #### Changed 20 | 21 | - Event handlers and fx are now traced, to provide more granular timing info in re-frame-trace. This has no impact on your code execution if you haven't [enabled tracing](https://github.com/day8/re-frame-trace#installation). 22 | 23 | ## 0.10.4 (2018.01.31) 24 | 25 | #### Changed 26 | 27 | - Interceptors, effects, and coeffects are now captured in re-frame's tracing. 28 | 29 | --- 30 | 31 | ## 0.10.3 (2018.01.24) 32 | 33 | #### New 34 | 35 | - add `purge-event-queue` to the API. See https://github.com/day8/re-frame-test/issues/13 for motivation. 36 | - added [a new FAQ entry](/docs/FAQs/DoINeedReFrame.md) Reagent looks terrific. Why do I need re-frame? 37 | - added [a new Infographic](/docs/AnEpoch.md) explaining how dominoes play out over time in the browser 38 | 39 | #### Changed 40 | 41 | - Debounce trace callbacks to handle larger batches of traces at once, to improve efficiency. 42 | - Improved error messages to not have multiple spaces before variables. 43 | 44 | #### Fixed 45 | 46 | - Handle js/performance not being defined in NodeJS. [#439](https://github.com/day8/re-frame/pull/439) 47 | - Improve cache eviction behaviour of subscription caches. In more complex applications a subscription may have been unnecessarily created and destroyed several times after a Figwheel re-render. 48 | -------------------------------------------------------------------------------- /docs/releases/2019.md: -------------------------------------------------------------------------------- 1 | 2 | 5 | # 6 | 7 | ## [0.11.0-rc3 (2019-11-20)](https://github.com/day8/re-frame/runs/311049839) 8 | 9 | #### Changed 10 | 11 | - Upgrade reagent to 0.9.0-rc3. [Report issues here](https://github.com/reagent-project/reagent/issues/449). 12 | - Update examples/simple/CodeWalkThrough.md 13 | - Upgrade shadow-cljs to 2.8.69 14 | - Migrate to [GitHub Actions](https://github.com/features/actions) 15 | 16 | --- 17 | 18 | ## 0.11.0-rc2 (2019-10-19) 19 | 20 | #### Changed 21 | 22 | - Upgrade shadow-cljs to 2.8.64 23 | - Upgrade karma to 4.4.1 24 | - Upgrade reagent to 0.9.0-rc2. [Report issues here](https://github.com/reagent-project/reagent/issues/449). 25 | 26 | --- 27 | ## 0.11.0-rc1 (2019-09-11) 28 | 29 | #### Changed 30 | 31 | - Upgrade reagent to 0.9.0-rc1. [Report issues here](https://github.com/reagent-project/reagent/issues/449). 32 | 33 | #### Removed 34 | 35 | - Remove direct dependency on react and react-dom as these are now provided by 36 | reagent. 37 | - Remove dependency on create-react-class as it is obsolete. 38 | 39 | --- 40 | 41 | ## 0.10.9 (2019-08-22) 42 | 43 | #### Changes 44 | 45 | - Migrate to [shadow-cljs](https://shadow-cljs.github.io/docs/UsersGuide.html) and 46 | [lein-shadow](https://gitlab.com/nikperic/lein-shadow) 47 | - Bring deps.edn into parity with project.clj 48 | 49 | --- 50 | 51 | ## 0.10.8 (2019-07-15) 52 | 53 | #### Changes 54 | 55 | - Revert `after` and `enrich` interceptors to the behaviour in 56 | version 0.10.6 and earlier; i.e. reverts breaking changes in 0.10.7. 57 | 58 | --- 59 | 60 | ## 0.10.7 (2019-06-24) 61 | 62 | #### BREAKING CHANGES 63 | 64 | - `after` and `enrich` interceptors now no longer run if there is no `db` effect, rather than running against the `db` coffect. [#453](https://github.com/day8/re-frame/issues/453) 65 | 66 | #### New 67 | 68 | - add `update-effect` [#440](https://github.com/day8/re-frame/issues/440) 69 | 70 | #### Fixed 71 | 72 | - [#512](https://github.com/day8/re-frame/issues/512) Error with infer-externs "goog is not defined in the externs" 73 | 74 | #### Improvements 75 | 76 | - Update dependency on reagent from `0.7.0` to `0.8.1`. 77 | - Update dependency on karma from `4.0.0` to `4.1.0`. 78 | - Update dependency on Clojure from `1.8.0` to `1.10.1`. 79 | - Update dependency on ClojureScript from `1.10.439` to `1.10.520`. 80 | -------------------------------------------------------------------------------- /docs/releases/2021.md: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | ## 1.2.0 (2021-02-25) 7 | 8 | #### Changed 9 | 10 | - Upgrade to [reagent 1.0.0](https://github.com/reagent-project/reagent/blob/master/CHANGELOG.md#100-2020-12-21) which includes [React 17.0.1](https://reactjs.org/blog/2020/10/20/react-v17.html). **Note:** also upgrade `re-frame-10x` to v1.0.0 when making the transition. 11 | - Upgrade clojure.tools.logging to 1.1.0 12 | 13 | -------------------------------------------------------------------------------- /docs/releases/2022.md: -------------------------------------------------------------------------------- 1 | 2 | 5 | # 6 | 7 | > The re-frame [Clojars page](https://clojars.org/re-frame/) contains dependency coordinates for Maven/deps/Lein. 8 | 9 | ## 1.3.0 (2022-08-27) 10 | 11 | 1.3.0-rc3 has been re-tagged as 1.3.0 final. 12 | 13 | ### rc3 (2022-03-04) 14 | 15 | #### Added 16 | 17 | - Warn user when calling subscribe outside of reactive context. See [#740](https://github.com/day8/re-frame/issues/740). Thanks to [@dannyfreeman](https://github.com/dannyfreeman). 18 | - Allow enrich to return `nil`. See [#751](https://github.com/day8/re-frame/pull/751). Thanks to [@NoahTheDuke](https://github.com/NoahTheDuke). 19 | 20 | ### rc2 (2021-12-22) 21 | 22 | #### Fixed 23 | 24 | - Fix some docs re recent reg-sub additions. Thanks to [@eval](https://github.com/eval). 25 | 26 | ### rc1 (2021-12-20) 27 | 28 | #### Added 29 | 30 | - Syntactic sugar for trivial reg-sub declarations. See [#634](https://github.com/day8/re-frame/pull/634). 31 | Thanks to [@bsboiko](https://github.com/bsboiko). 32 | 33 | #### Changed 34 | 35 | - Change `nil` `:fx` value to `:warn` instead of `:error` console log 36 | -------------------------------------------------------------------------------- /docs/releases/2023.md: -------------------------------------------------------------------------------- 1 | 2 | 5 | # 6 | 7 | > The re-frame [Clojars page](https://clojars.org/re-frame/) contains dependency coordinates for Maven/deps/Lein. 8 | 9 | ## 1.4.2 (2023-12-09) 10 | 11 | #### Fixed 12 | 13 | - `re-frame.utils` failed to load in a clj runtime, due to interop issues. 14 | 15 | ## 1.4.1 (2023-12-07) 16 | 17 | #### Added 18 | 19 | - `dispatch-sync` now emits a `:sync` trace to indicate when it has finished. See [re-frame-10x#165](https://github.com/day8/re-frame-10x/issues/165) 20 | 21 | ## 1.4.0 (2023-11-02) 22 | 23 | #### Breaking 24 | - `re-frame.std-interceptors/path` now keeps a `:re-frame.db/path-history` key in the context, not a `:re-frame-path/db-store` key. This shouldn't affect users, unless you're directly hacking the event loop. 25 | - [763](https://github.com/day8/re-frame/pull/763) on detecting an incorrect event structure, the `unwrap` interceptor now exceptions instead of writing an error to `js/console` and continuing. 26 | 27 | #### Added 28 | - Interceptors now have an optional `:comment` key. It's a no-op. 29 | - `re-frame.std-interceptors/path` now remembers the path it uses in a `:comment`. ([re-frame-10x#165](https://github.com/day8/re-frame-10x/issues/165)) 30 | - `re-frame.alpha` namespace, for testing proposed features (see [flows](https://github.com/day8/re-frame/discussions/795) and [polymorphic subscriptions](https://github.com/day8/re-frame/issues/680#issuecomment-1676487563)) 31 | 32 | #### Changed 33 | 34 | - Upgrade reagent to 1.2.0 35 | - Added `reg-event-error-handler` to provide a way to handle errors in the interceptor chain [#423](https://github.com/Day8/re-frame/pull/423). 36 | -------------------------------------------------------------------------------- /docs/releases/2024.md: -------------------------------------------------------------------------------- 1 | 2 | 5 | # 6 | 7 | > The re-frame [Clojars page](https://clojars.org/re-frame/) contains dependency coordinates for Maven/deps/Lein. 8 | 9 | ## 1.4.4 (2024-06-24) 10 | 11 | #### Changed 12 | 13 | - Changed flows to calculate the new app-db earlier in the chain. 14 | - Benefit: user-defined interceptors can access post-flow app-db value. 15 | - Drawback: they can't access the value of app-db directly resulting from the event handler. 16 | - Drawback: they can't access flow-related effects like `:reg-flow`. 17 | 18 | - Added a :re-frame/pre-flow-db key to the context 19 | - This way, user-defined interceptors can still access the app-db value resulting directly from the event handler. 20 | 21 | ## 1.4.3 (2024-01-25) 22 | 23 | #### Fixed 24 | 25 | - cljdoc analysis & build errors (see https://github.com/cljdoc/cljdoc/issues/853) 26 | -------------------------------------------------------------------------------- /docs/review-todomvc.md: -------------------------------------------------------------------------------- 1 | 2 | Congratulations! You now know everything necessary to enjoy the [todomvc example application](https://github.com/day8/re-frame/tree/master/examples/todomvc) within the re-frame repo. 3 | 4 | The code is heavily commented, and it will show you various re-frame features in action, together with providing useful practical advice. 5 | 6 | Don't skip this. Your review will be a very useful part of your learning process, and now is the right moment. 7 | -------------------------------------------------------------------------------- /docs/src/sci/configs/re_frame/re_frame.cljs: -------------------------------------------------------------------------------- 1 | (ns sci.configs.re-frame.re-frame 2 | (:require 3 | [re-frame.core] 4 | [re-frame.db] 5 | [re-frame.alpha] 6 | [sci.core :as sci])) 7 | 8 | (def rfns (sci/create-ns 're-frame.core nil)) 9 | (def rfdbns (sci/create-ns 're-frame.db nil)) 10 | (def rfa (sci/create-ns 're-frame.alpha nil)) 11 | 12 | (def re-frame-namespace 13 | (sci/copy-ns re-frame.core rfns)) 14 | 15 | (def re-frame-db-namespace 16 | (sci/copy-ns re-frame.db rfdbns)) 17 | 18 | (def re-frame-alpha-namespace 19 | (sci/copy-ns re-frame.alpha rfns)) 20 | 21 | (def namespaces 22 | {'re-frame.core re-frame-namespace 23 | 're-frame.db re-frame-db-namespace 24 | 're-frame.alpha re-frame-alpha-namespace}) 25 | 26 | (def config 27 | {:namespaces namespaces}) 28 | -------------------------------------------------------------------------------- /docs/start-coding.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Install Clojure 4 | 5 | Install Clojure and Leiningen (a build tool) by following [these instructions](https://purelyfunctional.tv/guide/how-to-install-clojure/). 6 | 7 | 8 | ## Use A Template 9 | 10 | Create a scaffold for your first project: 11 | 12 | - Client only: 13 | - Full Stack: 14 | 15 | 16 | ## Full Stack Example 17 | 18 | Take time to review a full stack application. [RealWorld](https://github.com/jacekschae/conduit) implements to the [RealWorld Spec and API](https://github.com/gothinkster/realworld). 19 | 20 | 21 | -------------------------------------------------------------------------------- /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: #2a83a3; 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 | -------------------------------------------------------------------------------- /docs/stylesheets/dominoes-live.css: -------------------------------------------------------------------------------- 1 | #dominoes-live-app { 2 | border-style: solid; 3 | border-color: #BBB; 4 | border-width: 1px; 5 | padding: 20px 6 | } 7 | 8 | #dominoes-live-app .preload { 9 | height: 273px; /* this height matches the final application height ... which stops items on the page moving around after load */ 10 | } 11 | 12 | #dominoes-live-app .example-clock { 13 | font-size: 128px; 14 | line-height: 1.2em; 15 | font-family: HelveticaNeue-UltraLight, Helvetica; 16 | } 17 | 18 | @media (max-width: 768px) { 19 | #dominoes-live-app .example-clock { 20 | font-size: 64px; 21 | } 22 | } 23 | 24 | #dominoes-live-app .color-input, 25 | #dominoes-live-app .color-input input { 26 | font-size: 24px; 27 | line-height: 1.5em; 28 | } 29 | -------------------------------------------------------------------------------- /docs/stylesheets/mkdocs.css: -------------------------------------------------------------------------------- 1 | 2 | /* Center ('space-evenly') links under home page logo. */ 3 | .tx-hero__links { 4 | display: flex; 5 | flow-direction: row; 6 | justify-content: space-evenly; 7 | } 8 | 9 | .tx-hero__links a { 10 | margin: 0 !important; 11 | } 12 | 13 | /* Hide empty h1 headers that we need to place at the top of some pages such 14 | as FAQs so that both a page-level header does NOT appear and the right 15 | hand side table of contents is not broken. */ 16 | h1[id=_1] { 17 | display: none; 18 | } 19 | 20 | /* 21 | */ 22 | .md-typeset h1, 23 | .md-typeset h2, 24 | .md-typeset h3 { 25 | color: #0075e1; 26 | font-family: 'Source Sans Pro', 'Open Sans', Helvetica, Arial, sans-serif; 27 | } 28 | 29 | .md-typeset h1 { 30 | font-weight: 200; 31 | letter-spacing: 0; 32 | font-size: 55px; 33 | margin: 0; 34 | } 35 | 36 | /* Trim whitespace above and below. In particular, less gap following paragraph. 37 | */ 38 | .md-typeset h2 { 39 | font-size: 35px; 40 | font-weight: 300; 41 | letter-spacing: 0; 42 | margin: 1.6rem 0 -0.2rem; 43 | border-bottom: solid 1px #e0f0ff; 44 | } 45 | 46 | 47 | /* slightly smaller font in code blocks */ 48 | .md-typeset code { 49 | font-size: .79em 50 | } 51 | 52 | 53 | /* 404 page */ 54 | .not-found-container { 55 | display: flex; 56 | flow-direction: row; 57 | } 58 | 59 | .not-found-text { 60 | margin-left: 31px; 61 | } 62 | 63 | /* The default gaps above and below paragraphs is fairly excessive. 64 | Default is about 1em. Reduce that slightly. 65 | */ 66 | .md-typeset p { 67 | margin-top: 0.75em; 68 | margin-bottom: 0.75em; 69 | } 70 | 71 | body { 72 | background-image: linear-gradient(rgb(255, 255, 255) 0%, rgb(255, 255, 255) 88%, rgb(251, 253, 255) 100%); 73 | } 74 | 75 | /*give he code blocks a bit more definition with a border */ 76 | .md-typeset pre>code { 77 | border: 1px solid #e4e4e4; 78 | } 79 | 80 | /* 81 | Bring the top of the code block "up" closer to the end of the paragraph above, 82 | leaving less whitespace. 83 | Paragraphs have a lower margin of about 0.75em (see above), so we are closing most of the gap. 84 | */ 85 | .codehilite { 86 | margin-top: -0.6em; 87 | } 88 | 89 | /* make the Nav font just fractionally smaller 90 | It was 0.7rem 91 | */ 92 | .md-nav { 93 | font-size: 0.65rem 94 | } 95 | 96 | -------------------------------------------------------------------------------- /docs/theme/404.html: -------------------------------------------------------------------------------- 1 | {#- 2 | This file was automatically generated - do not edit 3 | -#} 4 | {% extends "base.html" %} 5 | {% block content %} 6 |
7 | 8 |
9 |

Page Not Found

10 |

Someone is to blame for this.

11 |

Rest assured we have started the necessary witch hunt.

12 |
13 |
14 | 15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /docs/theme/main.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block htmltitle %} 4 | {% if page and page.meta and page.meta.title %} 5 | {{ page.meta.title }} 6 | {% elif page and page.title and not page.is_homepage %} 7 | {{ page.title | striptags }} 8 | {% else %} 9 | {{ config.site_name }} 10 | {% endif %} 11 | {% endblock %} 12 | 13 | 14 | 20 | {% block footer %} 21 | {% include "partials/footer.html" %} 22 | 23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /docs/theme/overrides/home.html: -------------------------------------------------------------------------------- 1 | {% extends "overrides/main.html" %} 2 | 3 | {% block tabs %} 4 | {{ super() }} 5 | 6 |
7 |
8 |
9 |
10 | 11 | 14 |
15 |
16 | 17 | 25 |
26 |
27 |

Build web apps, in ClojureScript, leveraging React.

28 |

Has a data-oriented, functional design.

29 |

Advanced enough to have outlasted three generations of Javascript technical churn.

30 |

A focus on developer productivity. Fewer lines of code. Hot code reloading. A simple dynamic model. Managed effects, including state. Pure functions. Variously declarative.

31 |

The more sophisticated your app, the better.

32 |
33 |
34 |
35 |
36 | {% endblock %} 37 | {% block content %}{% endblock %} 38 | {% block footer %}{% endblock %} 39 | -------------------------------------------------------------------------------- /docs/theme/overrides/main.html: -------------------------------------------------------------------------------- 1 | 19 | 20 | {% extends "base.html" %} 21 | 22 | 23 | {% block extrahead %} 24 | 25 | 26 | {% set title = config.site_name %} 27 | {% if page and page.title and not page.is_homepage %} 28 | {% set title = config.site_name ~ " - " ~ page.title | striptags %} 29 | {% endif %} 30 | 31 | 32 | {% set image = config.site_url ~ 'assets/images/banner.png' %} 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | {% endblock %} 52 | -------------------------------------------------------------------------------- /docs/theme/partials/footer.html: -------------------------------------------------------------------------------- 1 | 22 | 23 | {% import "partials/language.html" as lang with context %} 24 | 25 | 26 | 101 | -------------------------------------------------------------------------------- /docs/theme/partials/header.html: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 |
22 | 23 | 24 | 80 |
-------------------------------------------------------------------------------- /examples/flow/deps.edn: -------------------------------------------------------------------------------- 1 | {:deps {day8/re-frame {:local/root "../.."} 2 | reagent/reagent {:mvn/version "1.2.0"} 3 | thheller/shadow-cljs {:mvn/version "2.25.2"} 4 | hashp/hashp {:mvn/version "0.2.2"}}} 5 | -------------------------------------------------------------------------------- /examples/flow/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple", 3 | "scripts": { 4 | "watch": "npx shadow-cljs watch client", 5 | "release": "npx shadow-cljs release client", 6 | "build-report": "npx shadow-cljs run shadow.cljs.build-report client target/build-report.html" 7 | }, 8 | "devDependencies": { 9 | "karma": "^6.4.2", 10 | "karma-chrome-launcher": "3.1.0", 11 | "karma-cljs-test": "0.1.0", 12 | "karma-junit-reporter": "2.0.1", 13 | "shadow-cljs": "2.15.12" 14 | }, 15 | "dependencies": { 16 | "karma-puppeteer-launcher": "^1.0.4", 17 | "react": "^18.2.0", 18 | "react-dom": "^18.2.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/flow/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/flow/resources/public/index.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/flow/shadow-cljs.edn: -------------------------------------------------------------------------------- 1 | {:deps true 2 | :nrepl {:port 8777} 3 | 4 | :build-defaults 5 | {:build-hooks [(shadow-git-inject.core/hook)]} 6 | 7 | :dev-http 8 | {8280 "resources/public"} 9 | 10 | :builds 11 | {:client 12 | {:target :browser 13 | :output-dir "resources/public/js" 14 | :devtools 15 | {:preloads [hashp.core]} 16 | :modules 17 | {:client 18 | {:init-fn re-frame.flow.demo/run}}}}} 19 | -------------------------------------------------------------------------------- /examples/flow/src/re_frame/flow/demo.cljs: -------------------------------------------------------------------------------- 1 | (ns re-frame.flow.demo 2 | (:require 3 | [reagent.dom.client :as rdc] 4 | [re-frame.alpha :as rf] 5 | [zprint.core :as zp] 6 | [clojure.string :as str] 7 | [re-frame.db :refer [app-db]])) 8 | 9 | (defn debug-app-db [] 10 | (fn [] 11 | [:pre {:style {:position "absolute" :bottom 0 :right 0 :font-size 8}} 12 | (some-> app-db 13 | deref 14 | (zp/zprint-str {:style :justified}) 15 | (str/replace #"re-frame.flow.demo/" ":") 16 | (str/replace #"re-fine." ":"))])) 17 | 18 | (rf/reg-sub ::items :-> (comp reverse ::items)) 19 | 20 | (defn item [id] 21 | [:div "Item " id]) 22 | 23 | (defn items [] 24 | (into [:div] 25 | (map item) 26 | @(rf/subscribe [::items]))) 27 | 28 | (rf/reg-event-db 29 | ::clear-all 30 | (fn [db _] (dissoc db ::items))) 31 | 32 | (rf/reg-event-db 33 | ::add-item 34 | (fn [db [_ id]] (update db ::items conj id))) 35 | 36 | (rf/reg-event-db 37 | ::delete-item 38 | (fn [db [_ id]] (update db ::items #(remove #{id} %)))) 39 | 40 | (defn controls [] 41 | (let [id (atom 0)] 42 | (fn [] 43 | [:div 44 | [:button {:on-click #(do (rf/dispatch [::add-item (inc @id)]) 45 | (swap! id inc))} "Add"] " " 46 | [:button {:on-click #(do (rf/dispatch [::delete-item @id]) 47 | (swap! id dec))} "Delete"] " " 48 | [:button {:on-click #(do (rf/dispatch [::clear-all]) 49 | (reset! id 0))} "Clear"] " "]))) 50 | 51 | (def error-state-flow 52 | {:id ::error-state 53 | :path [::error-state] 54 | :inputs {:items [::items]} 55 | :output (fn [_ {:keys [items]}] 56 | (cond 57 | (> (count items) 2) :too-many 58 | (empty? items) :none)) 59 | :live-inputs {:items [::items]} 60 | :live? (fn [{:keys [items]}] 61 | (let [ct (count items)] 62 | (or (zero? ct) (> ct 3))))}) 63 | 64 | (rf/reg-flow error-state-flow) 65 | 66 | (rf/reg-event-fx 67 | ::clear-flow 68 | (fn [_ _] {:fx [[:clear-flow ::error-state]]})) 69 | 70 | (rf/reg-event-fx 71 | ::reg-flow 72 | (fn [_ _] {:fx [[:reg-flow error-state-flow]]})) 73 | 74 | (defn flow-controls [] 75 | [:div [:button {:on-click #(do (rf/dispatch [::clear-flow]))} 76 | "Clear flow"] " " 77 | [:button {:on-click #(do (rf/dispatch [::reg-flow]))} 78 | "Register flow"]]) 79 | 80 | (defn warning [] 81 | (let [error-state (rf/subscribe [:flow {:id ::error-state}])] 82 | [:div {:style {:color "red"}} 83 | (->> @error-state 84 | (get {:too-many "Warning: only the first 3 items will be used." 85 | :none "No items. Please add one."}))])) 86 | 87 | (defn root [] 88 | [:div [controls] [flow-controls] [warning] [items] [debug-app-db]]) 89 | 90 | (rf/reg-event-db 91 | ::init 92 | (fn [db _] db)) 93 | 94 | (defonce root-container 95 | (rdc/create-root (js/document.getElementById "app"))) 96 | 97 | (defn run 98 | [] 99 | (rf/dispatch-sync [::init]) 100 | (rdc/render root-container [root])) 101 | -------------------------------------------------------------------------------- /examples/simple/.gitignore: -------------------------------------------------------------------------------- 1 | .shadow-cljs/ 2 | -------------------------------------------------------------------------------- /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 | 7 | 8 | 9 | All the code is in one namespace: `/src/simple/core.cljs`. 10 | 11 | ### Run It 12 | 13 | Steps: 14 | 15 | 1. Install [Node.js](https://nodejs.org/en/) 16 | 2. `git clone https://github.com/day8/re-frame.git` 17 | 3. `cd re-frame/examples/simple` 18 | 4. Run "`npm install`" then "`npm run watch`" to compile the app and start up shadow-cljs hot-reloading 19 | 5. Wait for the compile to finish. At a minimum, 15 seconds. But, if you are new to ClojureScript and some downloads are needed (caches are empty), it might take a minute or two. Eventually you should see `[:client] Build Completed (...)` 20 | 6. Open `http://localhost:8280/` to see the app 21 | 22 | 23 | So, what just happened? 24 | 25 | The ClojureScript code under `/src` has been compiled into `javascript` and 26 | put into `/resources/public/js/client.js` which is loaded into `/resources/public/example.html` (the HTML file you just opened in the browser) 27 | 28 | Shadow-cljs (the compiler) provides for hot-reloading, which means you can edit the source code and 29 | immediately see the change in the browser. To test this, I'd suggest that you edit `./src/simple/core.cljs`, 30 | search for the string `"Hello world, it is now"`, change it to something else, save the file, then watch your 31 | change show up in the browser in near-realtime. 32 | 33 | ### Production Version 34 | 35 | Run "`npm run release`" to compile an optimised 36 | version, and then open `resources/public/index.html` in a browser. 37 | -------------------------------------------------------------------------------- /examples/simple/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | :deps {re-frame/re-frame {:local/root "../.."} 3 | reagent/reagent {:mvn/version "1.2.0"} 4 | thheller/shadow-cljs {:mvn/version "2.25.2"} 5 | day8/shadow-git-inject {:mvn/version "0.0.5"}}} 6 | -------------------------------------------------------------------------------- /examples/simple/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple", 3 | "scripts": { 4 | "watch": "npx shadow-cljs watch client", 5 | "release": "npx shadow-cljs release client", 6 | "build-report": "npx shadow-cljs run shadow.cljs.build-report client target/build-report.html" 7 | }, 8 | "devDependencies": { 9 | "karma": "^6.4.2", 10 | "karma-chrome-launcher": "3.1.0", 11 | "karma-cljs-test": "0.1.0", 12 | "karma-junit-reporter": "2.0.1", 13 | "shadow-cljs": "2.15.12" 14 | }, 15 | "dependencies": { 16 | "karma-puppeteer-launcher": "^1.0.4", 17 | "react": "^18.2.0", 18 | "react-dom": "^18.2.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /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/index.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/shadow-cljs.edn: -------------------------------------------------------------------------------- 1 | {:deps true 2 | :nrepl {:port 8777} 3 | 4 | :build-defaults 5 | {:build-hooks [(shadow-git-inject.core/hook)]} 6 | 7 | :dev-http 8 | {8280 "resources/public"} 9 | 10 | :builds 11 | {:client 12 | {:target :browser 13 | :output-dir "resources/public/js" 14 | :modules 15 | {:client 16 | {:init-fn simple.core/run}}}}} 17 | -------------------------------------------------------------------------------- /examples/simple/src/simple/core.cljs: -------------------------------------------------------------------------------- 1 | (ns simple.core 2 | (:require [reagent.dom.client :as rdc] 3 | [re-frame.core :as rf])) 4 | 5 | ;; A detailed walk-through of this source code is provided in the docs: 6 | ;; https://day8.github.io/re-frame/dominoes-live/ 7 | 8 | ;; -- Domino 1 - Event Dispatch ----------------------------------------------- 9 | 10 | (defn dispatch-timer-event 11 | [] 12 | (let [now (js/Date.)] 13 | (rf/dispatch [:timer now]))) ;; <-- dispatch used 14 | 15 | ;; Call the dispatching function every second. 16 | ;; `defonce` is like `def` but it ensures only one instance is ever 17 | ;; created in the face of figwheel hot-reloading of this file. 18 | (defonce do-timer (js/setInterval dispatch-timer-event 1000)) 19 | 20 | ;; -- Domino 2 - Event Handlers ----------------------------------------------- 21 | 22 | (rf/reg-event-db ;; sets up initial application state 23 | :initialize ;; usage: (dispatch [:initialize]) 24 | (fn [_ _] ;; the two parameters are not important here, so use _ 25 | {:time (js/Date.) ;; What it returns becomes the new application state 26 | :time-color "orange"})) ;; so the application state will initially be a map with two keys 27 | 28 | (rf/reg-event-db ;; usage: (dispatch [:time-color-change 34562]) 29 | :time-color-change ;; dispatched when the user enters a new colour into the UI text field 30 | (fn [db [_ new-color-value]] ;; -db event handlers given 2 parameters: current application state and event (a vector) 31 | (assoc db :time-color new-color-value))) ;; compute and return the new application state 32 | 33 | (rf/reg-event-db ;; usage: (dispatch [:timer a-js-Date]) 34 | :timer ;; every second an event of this kind will be dispatched 35 | (fn [db [_ new-time]] ;; note how the 2nd parameter is destructured to obtain the data value 36 | (assoc db :time new-time))) ;; compute and return the new application state 37 | 38 | ;; -- Domino 4 - Query ------------------------------------------------------- 39 | 40 | (rf/reg-sub 41 | :time 42 | (fn [db _] ;; db is current app state. 2nd unused param is query vector 43 | (:time db))) ;; return a query computation over the application state 44 | 45 | (rf/reg-sub 46 | :time-color 47 | (fn [db _] 48 | (:time-color db))) 49 | 50 | ;; -- Domino 5 - View Functions ---------------------------------------------- 51 | 52 | (defn clock 53 | [] 54 | (let [colour @(rf/subscribe [:time-color]) 55 | time (-> @(rf/subscribe [:time]) 56 | .toTimeString 57 | (clojure.string/split " ") 58 | first)] 59 | [:div.example-clock {:style {:color colour}} time])) 60 | 61 | (defn color-input 62 | [] 63 | (let [gettext (fn [e] (-> e .-target .-value)) 64 | emit (fn [e] (rf/dispatch [:time-color-change (gettext e)]))] 65 | [:div.color-input 66 | "Display color: " 67 | [:input {:type "text" 68 | :style {:border "1px solid #CCC"} 69 | :value @(rf/subscribe [:time-color]) ;; subscribe 70 | :on-change emit}]])) ;; <--- 71 | 72 | (defn ui 73 | [] 74 | [:div 75 | [:h1 "The time is now:"] 76 | [clock] 77 | [color-input]]) 78 | 79 | ;; -- Entry Point ------------------------------------------------------------- 80 | 81 | (defonce root-container 82 | (rdc/create-root (js/document.getElementById "app"))) 83 | 84 | (defn mount-ui 85 | [] 86 | (rdc/render root-container [ui])) 87 | 88 | (defn ^:dev/after-load clear-cache-and-render! 89 | [] 90 | ;; The `:dev/after-load` metadata causes this function to be called 91 | ;; after shadow-cljs hot-reloads code. We force a UI update by clearing 92 | ;; the Reframe subscription cache. 93 | (rf/clear-subscription-cache!) 94 | (mount-ui)) 95 | 96 | (defn run ;; Your app calls this when it starts. See shadow-cljs.edn :init-fn. 97 | [] 98 | (rf/dispatch-sync [:initialize]) ;; put a value into application state 99 | (mount-ui)) ;; mount the application's ui into '
' 100 | -------------------------------------------------------------------------------- /examples/simple/target/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/day8/re-frame/7b6fa64a6b3aecd2fcdbbfd80a89d59f92de57e1/examples/simple/target/.gitkeep -------------------------------------------------------------------------------- /examples/todomvc/.gitignore: -------------------------------------------------------------------------------- 1 | resources/public/js 2 | .shadow-cljs/ 3 | -------------------------------------------------------------------------------- /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 | ## Setup And Run A Development Environment 13 | 14 | 1. Install [Node.js](https://nodejs.org/en/) 15 | 16 | 2. Get the re-frame repo from GitHub: 17 | ```sh 18 | git clone https://github.com/day8/re-frame.git 19 | ``` 20 | 21 | 3. cd to the right example directory: 22 | ```sh 23 | cd re-frame/examples/todomvc 24 | ``` 25 | 26 | 4. Kick off a clean build (compile the app and start up shadow-cljs hot-reloading) 27 | ```sh 28 | npm install 29 | npm run watch 30 | ``` 31 | 32 | 5. Wait for the compile in step 4 to finish. At a minimum, 15 seconds. But, if you are new to ClojureScript and some downloads are needed (caches are empty), it might take a minute or two. Eventually you should see `[:client] Build Completed (...)` 33 | 34 | 6. Wait for step 5 to do the compile, and then open in UI in the browser: 35 | ```sh 36 | open http://localhost:8280 37 | ``` 38 | 39 | ## Exploring The Code 40 | 41 | From the re-frame docs: 42 | ``` 43 | To build a re-frame app, you: 44 | - design your app's data structure (data layer) 45 | - write and register subscription functions (query layer) 46 | - write Reagent component functions (view layer) 47 | - write and register event handler functions (control layer and/or state transition layer) 48 | - once in a blue moon you need to write effect handlers 49 | ``` 50 | 51 | In `src`, there's a matching set of files (each small): 52 | ``` 53 | src 54 | ├── core.cljs <--- entry point, plus history 55 | ├── db.cljs <--- data related (data layer) 56 | ├── subs.cljs <--- subscription handlers (query layer) 57 | ├── views.cljs <--- reagent components (view layer) 58 | └── events.cljs <--- event handlers (control/update layer) 59 | ``` 60 | 61 | For the most immediate feedback, edit some of the hiccup in the `views.cljs` file. When 62 | you save the file, shadow-cljs will immediately compile your changes and hot-reload them into the browser. 63 | 64 | 65 | ## Further Notes 66 | 67 | The [official reagent example](https://github.com/reagent-project/reagent/tree/master/examples/todomvc) which is very terse indeed, can be found here. 68 | 69 | 70 | ## To Compile An Optimised Version 71 | 72 | 1. Compile 73 | ```sh 74 | npm install 75 | npm run release 76 | ``` 77 | 78 | 2. Open the following in your browser 79 | ```sh 80 | resources/public/index.html 81 | ``` 82 | -------------------------------------------------------------------------------- /examples/todomvc/deps.edn: -------------------------------------------------------------------------------- 1 | {:deps {day8/re-frame {:local/root "../.."} 2 | reagent/reagent {:mvn/version "1.2.0"} 3 | thheller/shadow-cljs {:mvn/version "2.25.2"} 4 | clj-commons/secretary {:mvn/version "1.2.4"} 5 | day8.re-frame/tracing {:mvn/version "0.6.2"} 6 | day8.re-frame/re-frame-10x {:mvn/version "1.8.1"} 7 | day8/shadow-git-inject {:mvn/version "0.0.5"}}} 8 | -------------------------------------------------------------------------------- /examples/todomvc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todomvc-re-frame", 3 | "scripts": { 4 | "watch": "npx shadow-cljs watch client", 5 | "release": "npx shadow-cljs release client", 6 | "build-report": "npx shadow-cljs run shadow.cljs.build-report client target/build-report.html" 7 | }, 8 | "devDependencies": { 9 | "karma": "^6.4.2", 10 | "karma-chrome-launcher": "3.1.0", 11 | "karma-cljs-test": "0.1.0", 12 | "karma-junit-reporter": "2.0.1", 13 | "shadow-cljs": "2.15.12" 14 | }, 15 | "dependencies": { 16 | "karma-puppeteer-launcher": "^1.0.4", 17 | "react": "^18.2.0", 18 | "react-dom": "^18.2.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /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/shadow-cljs.edn: -------------------------------------------------------------------------------- 1 | {:nrepl 2 | {:port 8777} 3 | 4 | :deps true 5 | 6 | :build-defaults 7 | {:build-hooks [(shadow-git-inject.core/hook)]} 8 | 9 | :dev-http 10 | {8280 "resources/public"} 11 | 12 | :builds 13 | {:client 14 | {:target :browser 15 | :output-dir "resources/public/js" 16 | :modules 17 | {:client 18 | {:init-fn todomvc.core/main}} 19 | :dev 20 | {:compiler-options 21 | {:closure-defines 22 | {re-frame.trace.trace-enabled? true 23 | day8.re-frame.tracing.trace-enabled? true}}} 24 | :release 25 | {:build-options 26 | {:ns-aliases 27 | {day8.re-frame.tracing day8.re-frame.tracing-stubs}}}}}} 28 | -------------------------------------------------------------------------------- /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.dom.client :as rdc] 5 | [re-frame.alpha :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 | (:import [goog History] 11 | [goog.history EventType])) 12 | 13 | ;; -- Debugging aids ---------------------------------------------------------- 14 | (enable-console-print!) ;; so that println writes to `console.log` 15 | 16 | ;; Put an initial value into app-db. 17 | ;; The event handler for `:initialise-db` can be found in `events.cljs` 18 | ;; Using the sync version of dispatch means that value is in 19 | ;; place before we go onto the next step. 20 | (dispatch-sync [:initialise-db]) 21 | 22 | ;; -- Routes and History ------------------------------------------------------ 23 | ;; Although we use the secretary library below, that's mostly a historical 24 | ;; accident. You might also consider using: 25 | ;; - https://github.com/DomKM/silk 26 | ;; - https://github.com/juxt/bidi 27 | ;; We don't have a strong opinion. 28 | ;; 29 | (defroute "/" [] (dispatch [:set-showing :all])) 30 | (defroute "/:filter" [filter] (dispatch [:set-showing (keyword filter)])) 31 | 32 | (defonce history 33 | (doto (History.) 34 | (events/listen EventType.NAVIGATE 35 | (fn [^js/goog.History.Event event] (secretary/dispatch! (.-token event)))) 36 | (.setEnabled true))) 37 | 38 | ;; -- Entry Point ------------------------------------------------------------- 39 | 40 | (defonce root-container 41 | (rdc/create-root (.getElementById js/document "app"))) 42 | 43 | (defn render 44 | [] 45 | ;; Render the UI into the HTML's
element 46 | ;; The view function `todomvc.views/todo-app` is the 47 | ;; root view for the entire UI. 48 | (rdc/render root-container [todomvc.views/todo-app])) 49 | 50 | (defn ^:dev/after-load clear-cache-and-render! 51 | [] 52 | ;; The `:dev/after-load` metadata causes this function to be called 53 | ;; after shadow-cljs hot-reloads code. We force a UI update by clearing 54 | ;; the Reframe subscription cache. 55 | (rf/clear-subscription-cache!) 56 | (render)) 57 | 58 | (defn ^:export main 59 | [] 60 | (render)) 61 | -------------------------------------------------------------------------------- /examples/todomvc/src/todomvc/db.cljs: -------------------------------------------------------------------------------- 1 | (ns todomvc.db 2 | (:require [cljs.reader] 3 | [cljs.spec.alpha :as s] 4 | [re-frame.alpha :as re-frame])) 5 | 6 | ;; -- Spec -------------------------------------------------------------------- 7 | ;; 8 | ;; This is a clojure.spec specification for the value in app-db. It is like a 9 | ;; Schema. See: http://clojure.org/guides/spec 10 | ;; 11 | ;; The value in app-db should always match this spec. Only event handlers 12 | ;; can change the value in app-db so, after each event handler 13 | ;; has run, we re-check app-db for correctness (compliance with the Schema). 14 | ;; 15 | ;; How is this done? Look in events.cljs and you'll notice that all handlers 16 | ;; have an "after" interceptor which does the spec re-check. 17 | ;; 18 | ;; None of this is strictly necessary. It could be omitted. But we find it 19 | ;; good practice. 20 | 21 | (s/def ::id int?) 22 | (s/def ::title string?) 23 | (s/def ::done boolean?) 24 | (s/def ::todo (s/keys :req-un [::id ::title ::done])) 25 | (s/def ::todos (s/and ;; should use the :kind kw to s/map-of (not supported yet) 26 | (s/map-of ::id ::todo) ;; in this map, each todo is keyed by its :id 27 | #(instance? PersistentTreeMap %) ;; is a sorted-map (not just a map) 28 | )) 29 | (s/def ::showing ;; what todos are shown to the user? 30 | #{:all ;; all todos are shown 31 | :active ;; only todos whose :done is false 32 | :done ;; only todos whose :done is true 33 | }) 34 | (s/def ::db (s/keys :req-un [::todos ::showing])) 35 | 36 | ;; -- Default app-db Value --------------------------------------------------- 37 | ;; 38 | ;; When the application first starts, this will be the value put in app-db 39 | ;; Unless, of course, there are todos in the LocalStore (see further below) 40 | ;; Look in: 41 | ;; 1. `core.cljs` for "(dispatch-sync [:initialise-db])" 42 | ;; 2. `events.cljs` for the registration of :initialise-db handler 43 | ;; 44 | 45 | (def default-db ;; what gets put into app-db by default. 46 | {:todos (sorted-map) ;; an empty list of todos. Use the (int) :id as the key 47 | :showing :all}) ;; show all todos 48 | 49 | ;; -- Local Storage ---------------------------------------------------------- 50 | ;; 51 | ;; Part of the todomvc challenge is to store todos in LocalStorage, and 52 | ;; on app startup, reload the todos from when the program was last run. 53 | ;; But the challenge stipulates to NOT load the setting for the "showing" 54 | ;; filter. Just the todos. 55 | ;; 56 | 57 | (def ls-key "todos-reframe") ;; localstore key 58 | 59 | (defn todos->local-store 60 | "Puts todos into localStorage" 61 | [todos] 62 | (.setItem js/localStorage ls-key (str todos))) ;; sorted-map written as an EDN map 63 | 64 | ;; -- cofx Registrations ----------------------------------------------------- 65 | 66 | ;; Use `reg-cofx` to register a "coeffect handler" which will inject the todos 67 | ;; stored in localstore. 68 | ;; 69 | ;; To see it used, look in `events.cljs` at the event handler for `:initialise-db`. 70 | ;; That event handler has the interceptor `(inject-cofx :local-store-todos)` 71 | ;; The function registered below will be used to fulfill that request. 72 | ;; 73 | ;; We must supply a `sorted-map` but in LocalStore it is stored as a `map`. 74 | ;; 75 | (re-frame/reg-cofx 76 | :local-store-todos 77 | (fn [cofx _] 78 | ;; put the localstore todos into the coeffect under :local-store-todos 79 | (assoc cofx :local-store-todos 80 | ;; read in todos from localstore, and process into a sorted map 81 | (into (sorted-map) 82 | (some->> (.getItem js/localStorage ls-key) 83 | (cljs.reader/read-string) ;; EDN map -> map 84 | ))))) 85 | -------------------------------------------------------------------------------- /examples/todomvc/src/todomvc/views.cljs: -------------------------------------------------------------------------------- 1 | (ns todomvc.views 2 | (:require [reagent.core :as reagent] 3 | [re-frame.alpha :refer [subscribe dispatch sub]] 4 | [clojure.string :as str])) 5 | 6 | (defn todo-input [{:keys [title on-save on-stop]}] 7 | (let [val (reagent/atom title) 8 | stop #(do (reset! val "") 9 | (when on-stop (on-stop))) 10 | save #(let [v (-> @val str str/trim)] 11 | (on-save v) 12 | (stop))] 13 | (fn [props] 14 | [:input (merge (dissoc props :on-save :on-stop :title) 15 | {:type "text" 16 | :value @val 17 | :auto-focus true 18 | :on-blur save 19 | :on-change #(reset! val (-> % .-target .-value)) 20 | :on-key-down #(case (.-which %) 21 | 13 (save) 22 | 27 (stop) 23 | nil)})]))) 24 | 25 | (defn todo-item 26 | [] 27 | (let [editing (reagent/atom false)] 28 | (fn [{:keys [id done title]}] 29 | [:li {:class (str (when done "completed ") 30 | (when @editing "editing"))} 31 | [:div.view 32 | [:input.toggle 33 | {:type "checkbox" 34 | :checked done 35 | :on-change #(dispatch [:toggle-done id])}] 36 | [:label 37 | {:on-double-click #(reset! editing true)} 38 | title] 39 | [:button.destroy 40 | {:on-click #(dispatch [:delete-todo id])}]] 41 | (when @editing 42 | [todo-input 43 | {:class "edit" 44 | :title title 45 | :on-save #(if (seq %) 46 | (dispatch [:save id %]) 47 | (dispatch [:delete-todo id])) 48 | :on-stop #(reset! editing false)}])]))) 49 | 50 | (defn task-list 51 | [] 52 | (let [visible-todos @(subscribe [:visible-todos]) 53 | all-complete? @(subscribe [:all-complete?])] 54 | [:section#main 55 | [:input#toggle-all 56 | {:type "checkbox" 57 | :checked all-complete? 58 | :on-change #(dispatch [:complete-all-toggle])}] 59 | [:label 60 | {:for "toggle-all"} 61 | "Mark all as complete"] 62 | [:ul#todo-list 63 | (for [todo visible-todos] 64 | ^{:key (:id todo)} [todo-item todo])]])) 65 | 66 | (defn footer-controls 67 | [] 68 | (let [[active done] @(subscribe [:footer-counts]) 69 | showing @(subscribe [:showing]) 70 | a-fn (fn [filter-kw txt] 71 | [:a {:class (when (= filter-kw showing) "selected") 72 | :href (str "#/" (name filter-kw))} txt])] 73 | [:footer#footer 74 | [:span#todo-count 75 | [:strong active] " " (case active 1 "item" "items") " left"] 76 | [:ul#filters 77 | [:li (a-fn :all "All")] 78 | [:li (a-fn :active "Active")] 79 | [:li (a-fn :done "Completed")]] 80 | (when (pos? done) 81 | [:button#clear-completed {:on-click #(dispatch [:clear-completed])} 82 | "Clear completed"])])) 83 | 84 | (defn task-entry 85 | [] 86 | [:header#header 87 | [:h1 "todos"] 88 | [todo-input 89 | {:id "new-todo" 90 | :placeholder "What needs to be done?" 91 | :on-save #(when (seq %) 92 | (dispatch [:add-todo %]))}]]) 93 | 94 | (defn alpha [] 95 | (let [alpha? @(sub :alpha?)] 96 | [:a {:href "#" 97 | :style {:color (if alpha? "red" "gray")} 98 | :on-click #(dispatch [:toggle-alpha])} 99 | (if alpha? "alpha is running!" "try alpha?")])) 100 | 101 | (defn todo-app 102 | [] 103 | [:<> 104 | [alpha] 105 | [:section#todoapp 106 | [task-entry] 107 | (when (seq @(subscribe [:todos])) 108 | [task-list]) 109 | [footer-controls]] 110 | [:footer#info 111 | [:p "Double-click to edit a todo"]]]) 112 | -------------------------------------------------------------------------------- /examples/todomvc/target/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/day8/re-frame/7b6fa64a6b3aecd2fcdbbfd80a89d59f92de57e1/examples/todomvc/target/.gitkeep -------------------------------------------------------------------------------- /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-puppeteer-launcher', 15 | 'karma-chrome-launcher', 16 | 'karma-junit-reporter' 17 | ], 18 | colors: true, 19 | logLevel: config.LOG_INFO, 20 | client: { 21 | args: ['shadow.test.karma.init'], 22 | singleRun: true 23 | }, 24 | 25 | // the default configuration 26 | junitReporter: { 27 | outputDir: junitOutputDir + '/karma', // results will be saved as outputDir/browserName.xml 28 | outputFile: undefined, // if included, results will be saved as outputDir/browserName/outputFile 29 | suite: '' // suite will become the package name attribute in xml testsuite element 30 | } 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2021 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "re-frame", 3 | "devDependencies": { 4 | "karma": "6.4.1", 5 | "karma-chrome-launcher": "3.1.1", 6 | "karma-cljs-test": "0.1.0", 7 | "karma-junit-reporter": "2.0.1", 8 | "karma-puppeteer-launcher": "1.0.4", 9 | "shadow-cljs": "2.20.7" 10 | }, 11 | "dependencies": { 12 | "@nextjournal/lang-clojure": "^1.0.0", 13 | "codemirror": "^6.0.1", 14 | "react": "^18.2.0", 15 | "react-dom": "^18.2.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /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.11.1" :scope "provided"] 7 | [org.clojure/clojurescript ~(or (System/getenv "CANARY_CLOJURESCRIPT_VERSION") "1.11.60") 8 | :scope "provided" 9 | :exclusions [com.google.javascript/closure-compiler-unshaded 10 | org.clojure/google-closure-library 11 | org.clojure/google-closure-library-third-party]] 12 | [thheller/shadow-cljs "2.23.3" :scope "provided"] 13 | [reagent/reagent "1.2.0"] 14 | [net.cgrand/macrovich "0.2.1"] 15 | [org.clojure/tools.logging "1.2.4"] 16 | [org.babashka/sci "0.8.40" :scope "provided"] 17 | [funcool/promesa "10.0.575" :scope "provided"]] 18 | 19 | :plugins [[day8/lein-git-inject "0.0.15"] 20 | [lein-shadow "0.4.1"]] 21 | 22 | :middleware [leiningen.git-inject/middleware] 23 | 24 | :git-inject {:version-pattern #"v(\d+\.\d+\.\d+.*)"} 25 | 26 | :profiles {:debug {:debug true} 27 | :dev {:dependencies [[binaryage/devtools "1.0.3"]] 28 | :plugins [[com.github.liquidz/antq "RELEASE"] 29 | [lein-shell "0.5.0"]] 30 | :antq {}} 31 | :docs {:dependencies [[org.babashka/sci "0.8.40"] 32 | [funcool/promesa "10.0.575"]]}} 33 | 34 | :clean-targets [:target-path 35 | "shadow-cljs.edn" 36 | "node_modules" 37 | "run/compiled"] 38 | 39 | :resource-paths ["resources"] 40 | :jvm-opts ["-Xmx1g"] 41 | :source-paths ["src" "docs/src"] ;; FixMe: Only the docs build should use the docs path. 42 | :test-paths ["test"] 43 | 44 | :shell {:commands {"karma" {:windows ["cmd" "/c" "karma"] 45 | :default-command "karma"} 46 | "open" {:windows ["cmd" "/c" "start"] 47 | :macosx "open" 48 | :linux "xdg-open"}}} 49 | 50 | :deploy-repositories [["clojars" {:sign-releases false 51 | :url "https://clojars.org/repo" 52 | :username :env/CLOJARS_USERNAME 53 | :password :env/CLOJARS_TOKEN}]] 54 | 55 | :release-tasks [["deploy" "clojars"]] 56 | 57 | :shadow-cljs {:nrepl {:port 8777} 58 | 59 | :builds {:docs 60 | {:target :browser 61 | :devtools {:repl-pprint true} 62 | :modules {:docs {:entries [re-frame.docs]}} 63 | :output-dir "docs/js"} 64 | :browser-test 65 | {:target :browser-test 66 | :ns-regexp "re-frame\\..*-test$" 67 | :test-dir "run/compiled/browser/test" 68 | :compiler-options {:pretty-print true 69 | :external-config {:devtools/config {:features-to-install [:formatters :hints]}}} 70 | :devtools {:http-port 3449 71 | :http-root "run/compiled/browser/test" 72 | :preloads [devtools.preload]}} 73 | 74 | :karma-test 75 | {:target :karma 76 | :ns-regexp "re-frame\\..*-test$" 77 | :output-to "run/compiled/karma/test/test.js" 78 | :compiler-options {:pretty-print true 79 | :closure-defines {re-frame.trace.trace-enabled? true}}}}} 80 | 81 | :aliases {"watch" ["do" 82 | ["clean"] 83 | ["with-profile" "+docs" "shadow" "watch" "browser-test" "karma-test" "docs"]] 84 | 85 | "ci" ["do" 86 | ["clean"] 87 | ["shadow" "compile" "karma-test"] 88 | ["shell" "karma" "start" "--single-run" "--reporters" "junit,dots"]] 89 | 90 | "docs" ["do" 91 | ["clean"] 92 | ["with-profile" "+docs" "shadow" "release" ":docs"]]}) 93 | -------------------------------------------------------------------------------- /src/deps.cljs: -------------------------------------------------------------------------------- 1 | {:npm-dev-deps {"shadow-cljs" "2.20.7" 2 | "karma" "6.4.1" 3 | "karma-puppeteer-launcher" "1.0.4" 4 | "karma-chrome-launcher" "3.1.1" 5 | "karma-cljs-test" "0.1.0" 6 | "karma-junit-reporter" "2.0.1"}} 7 | -------------------------------------------------------------------------------- /src/re_frame/cofx.cljc: -------------------------------------------------------------------------------- 1 | (ns re-frame.cofx 2 | (:require 3 | [re-frame.db :refer [app-db]] 4 | [re-frame.interceptor :refer [->interceptor]] 5 | [re-frame.registrar :refer [get-handler register-handler]] 6 | [re-frame.loggers :refer [console]])) 7 | 8 | ;; -- Registration ------------------------------------------------------------ 9 | 10 | (def kind :cofx) 11 | (assert (re-frame.registrar/kinds kind)) 12 | 13 | (defn reg-cofx 14 | [id handler] 15 | (register-handler kind id handler)) 16 | 17 | ;; -- Interceptor ------------------------------------------------------------- 18 | 19 | (defn inject-cofx 20 | ([id] 21 | (->interceptor 22 | :id :coeffects 23 | :before (fn coeffects-before 24 | [context] 25 | (if-let [handler (get-handler kind id)] 26 | (update context :coeffects handler) 27 | (console :error "No cofx handler registered for" id))))) 28 | ([id value] 29 | (->interceptor 30 | :id :coeffects 31 | :before (fn coeffects-before 32 | [context] 33 | (if-let [handler (get-handler kind id)] 34 | (update context :coeffects handler value) 35 | (console :error "No cofx handler registered for" id)))))) 36 | 37 | ;; -- Builtin CoEffects Handlers --------------------------------------------- 38 | 39 | ;; :db 40 | ;; 41 | ;; Adds to coeffects the value in `app-db`, under the key `:db` 42 | (reg-cofx 43 | :db 44 | (fn db-coeffects-handler 45 | [coeffects] 46 | (assoc coeffects :db @app-db))) 47 | 48 | ;; Because this interceptor is used so much, we reify it 49 | (def inject-db (inject-cofx :db)) 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/re_frame/db.cljc: -------------------------------------------------------------------------------- 1 | (ns re-frame.db 2 | (:require [re-frame.interop :refer [ratom]])) 3 | 4 | ;; -- Application State -------------------------------------------------------------------------- 5 | ;; 6 | ;; Should not be accessed directly by application code. 7 | ;; Read access goes through subscriptions. 8 | ;; Updates via event handlers. 9 | (def app-db (ratom {})) 10 | 11 | -------------------------------------------------------------------------------- /src/re_frame/events.cljc: -------------------------------------------------------------------------------- 1 | (ns re-frame.events 2 | (:require [re-frame.db :refer [app-db]] 3 | [re-frame.utils :refer [first-in-vector]] 4 | [re-frame.interop :refer [empty-queue debug-enabled?]] 5 | [re-frame.registrar :refer [get-handler register-handler]] 6 | [re-frame.loggers :refer [console]] 7 | [re-frame.interceptor :as interceptor] 8 | [re-frame.trace :as trace :include-macros true])) 9 | 10 | (def kind :event) 11 | (assert (re-frame.registrar/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 | (defn register 34 | "Associate the given event `id` with the given collection of `interceptors`. 35 | 36 | `interceptors` may contain nested collections and there may be nils 37 | at any level,so process this structure into a simple, nil-less vector 38 | before registration. 39 | 40 | Typically, an `event handler` will be at the end of the chain (wrapped 41 | in an interceptor)." 42 | [id interceptors] 43 | (register-handler kind id (flatten-and-remove-nils id interceptors))) 44 | 45 | ;; -- handle event -------------------------------------------------------------------------------- 46 | 47 | (def ^:dynamic *handling* nil) ;; remember what event we are currently handling 48 | 49 | (defn handle 50 | "Given an event vector `event-v`, look up the associated interceptor chain, and execute it." 51 | [event-v] 52 | (let [event-id (first-in-vector event-v)] 53 | (if-let [interceptors (get-handler kind event-id true)] 54 | (if *handling* 55 | (console :error "re-frame: while handling" *handling* ", dispatch-sync was called for" event-v ". You can't call dispatch-sync within an event handler.") 56 | (binding [*handling* event-v] 57 | (trace/with-trace {:operation event-id 58 | :op-type kind 59 | :tags {:event event-v}} 60 | (trace/merge-trace! {:tags {:app-db-before @app-db}}) 61 | (interceptor/execute event-v interceptors) 62 | (trace/merge-trace! {:tags {:app-db-after @app-db}}))))))) 63 | 64 | 65 | -------------------------------------------------------------------------------- /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 | (defn on-load 23 | [listener]) ;; no-op 24 | 25 | (defonce ^:private executor (Executors/newSingleThreadExecutor)) 26 | 27 | (defonce ^:private on-dispose-callbacks (atom {})) 28 | 29 | (defn next-tick [f] 30 | (let [bound-f (bound-fn [& args] (apply f args))] 31 | (.execute ^Executor executor bound-f)) 32 | nil) 33 | 34 | (def empty-queue clojure.lang.PersistentQueue/EMPTY) 35 | 36 | (def after-render next-tick) 37 | 38 | (def debug-enabled? true) 39 | 40 | (defn ratom [x] 41 | (atom x)) 42 | 43 | (defn ratom? [x] 44 | (instance? clojure.lang.IAtom x)) 45 | 46 | (defn deref? [x] 47 | (instance? clojure.lang.IDeref x)) 48 | 49 | (defn make-reaction 50 | "On JVM Clojure, return a `deref`-able thing which invokes the given function 51 | on every `deref`. That is, `make-reaction` here provides precisely none of the 52 | benefits of `reagent.ratom/make-reaction` (which only invokes its function if 53 | the reactions that the function derefs have changed value). But so long as `f` 54 | only depends on other reactions (which also behave themselves), the only 55 | difference is one of efficiency. That is, your tests should see no difference 56 | other than that they do redundant work." 57 | [f] 58 | (reify clojure.lang.IDeref 59 | (deref [_] (f)))) 60 | 61 | (defn add-on-dispose! 62 | "On JVM Clojure, use an atom to register `f` to be invoked when `dispose!` is 63 | invoked with `a-ratom`." 64 | [a-ratom f] 65 | (do (swap! on-dispose-callbacks update a-ratom (fnil conj []) f) 66 | nil)) 67 | 68 | (defn dispose! 69 | "On JVM Clojure, invoke all callbacks registered with `add-on-dispose!` for 70 | `a-ratom`." 71 | [a-ratom] 72 | ;; Try to replicate reagent's behavior, releasing resources first then 73 | ;; invoking callbacks 74 | (let [callbacks (get @on-dispose-callbacks a-ratom)] 75 | (swap! on-dispose-callbacks dissoc a-ratom) 76 | (doseq [f callbacks] (f)))) 77 | 78 | (defn set-timeout! 79 | "Note that we ignore the `ms` value and just invoke the function, because 80 | there isn't often much point firing a timed event in a test." 81 | [f ms] 82 | (next-tick f)) 83 | 84 | (defn now [] 85 | ;; currentTimeMillis may count backwards in some scenarios, but as this is used for tracing 86 | ;; it is preferable to the slower but more accurate System.nanoTime. 87 | (System/currentTimeMillis)) 88 | 89 | (defn reagent-id 90 | "Doesn't make sense in a Clojure context currently." 91 | [reactive-val] 92 | "rx-clj") 93 | 94 | (defn reactive? 95 | [] 96 | true) 97 | -------------------------------------------------------------------------------- /src/re_frame/interop.cljs: -------------------------------------------------------------------------------- 1 | (ns re-frame.interop 2 | (:require [goog.async.nextTick] 3 | [goog.events :as events] 4 | [reagent.core] 5 | [reagent.ratom])) 6 | 7 | (defn on-load 8 | [listener] 9 | ;; events/listen throws an exception in react-native environments because addEventListener is not available. 10 | (try 11 | (events/listen js/self "load" listener) 12 | (catch :default _))) 13 | 14 | (def next-tick goog.async.nextTick) 15 | 16 | (def empty-queue #queue []) 17 | 18 | (def after-render reagent.core/after-render) 19 | 20 | ;; Make sure the Google Closure compiler sees this as a boolean constant, 21 | ;; otherwise Dead Code Elimination won't happen in `:advanced` builds. 22 | ;; Type hints have been liberally sprinkled. 23 | ;; https://developers.google.com/closure/compiler/docs/js-for-compiler 24 | (def ^boolean debug-enabled? "@define {boolean}" ^boolean goog/DEBUG) 25 | 26 | (defn ratom [x] 27 | (reagent.core/atom x)) 28 | 29 | (defn ratom? [x] 30 | ;; ^:js suppresses externs inference warnings by forcing the compiler to 31 | ;; generate proper externs. Although not strictly required as 32 | ;; reagent.ratom/IReactiveAtom is not JS interop it appears to be harmless. 33 | ;; See https://shadow-cljs.github.io/docs/UsersGuide.html#infer-externs 34 | (satisfies? reagent.ratom/IReactiveAtom ^js x)) 35 | 36 | (defn deref? [x] 37 | (satisfies? IDeref x)) 38 | 39 | (defn make-reaction [f] 40 | (reagent.ratom/make-reaction f)) 41 | 42 | (defn add-on-dispose! [a-ratom f] 43 | (reagent.ratom/add-on-dispose! a-ratom f)) 44 | 45 | (defn dispose! [a-ratom] 46 | (reagent.ratom/dispose! a-ratom)) 47 | 48 | (defn set-timeout! [f ms] 49 | (js/setTimeout f ms)) 50 | 51 | (defn now [] 52 | (if (and 53 | (exists? js/performance) 54 | (exists? js/performance.now)) 55 | (js/performance.now) 56 | (js/Date.now))) 57 | 58 | (defn reagent-id 59 | "Produces an id for reactive Reagent values 60 | e.g. reactions, ratoms, cursors." 61 | [reactive-val] 62 | ;; ^:js suppresses externs inference warnings by forcing the compiler to 63 | ;; generate proper externs. Although not strictly required as 64 | ;; reagent.ratom/IReactiveAtom is not JS interop it appears to be harmless. 65 | ;; See https://shadow-cljs.github.io/docs/UsersGuide.html#infer-externs 66 | (when (implements? reagent.ratom/IReactiveAtom ^js reactive-val) 67 | (str (condp instance? reactive-val 68 | reagent.ratom/RAtom "ra" 69 | reagent.ratom/RCursor "rc" 70 | reagent.ratom/Reaction "rx" 71 | reagent.ratom/Track "tr" 72 | "other") 73 | (hash reactive-val)))) 74 | 75 | (defn reactive? 76 | [] 77 | (reagent.ratom/reactive?)) 78 | -------------------------------------------------------------------------------- /src/re_frame/loggers.cljc: -------------------------------------------------------------------------------- 1 | (ns re-frame.loggers 2 | (:require 3 | [clojure.set :refer [difference]] 4 | #?@(:clj [[clojure.string :as str] 5 | [clojure.tools.logging :as log]]))) 6 | 7 | #?(:clj (defn log [level & args] 8 | (log/log level (if (= 1 (count args)) 9 | (first args) 10 | (str/join " " args))))) 11 | 12 | ;; 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 | (defn set-loggers! 42 | [new-loggers] 43 | (assert (empty? (difference (set (keys new-loggers)) (-> @loggers keys set))) "Unknown keys in new-loggers") 44 | (swap! loggers merge new-loggers)) 45 | 46 | (defn get-loggers 47 | "Get the current logging functions used by re-frame." 48 | [] 49 | @loggers) 50 | -------------------------------------------------------------------------------- /src/re_frame/query/alpha.cljc: -------------------------------------------------------------------------------- 1 | (ns re-frame.query.alpha 2 | (:require 3 | [re-frame :as-alias rf] 4 | [re-frame.db :refer [app-db]] 5 | [re-frame.interop :refer [reagent-id]] 6 | [re-frame.loggers :refer [console]] 7 | [re-frame.register.alpha :refer [lifecycle->method]] 8 | [re-frame.registrar :refer [get-handler]] 9 | [re-frame.trace :as trace :include-macros true])) 10 | 11 | (declare lifecycle) 12 | 13 | (defn legacy-lifecycle [v] 14 | (when (vector? v) 15 | (or (lifecycle (meta v)) 16 | :default))) 17 | 18 | (defn legacy-query-id [q] 19 | (when (vector? q) (first q))) 20 | 21 | (def id (some-fn legacy-query-id ::rf/q)) 22 | 23 | (def flow-lifecycle (comp #{:flow} id)) 24 | 25 | (def lifecycle (some-fn flow-lifecycle 26 | legacy-lifecycle 27 | ::rf/lifecycle 28 | (constantly :default))) 29 | 30 | (defn method [q] (@lifecycle->method (lifecycle q))) 31 | 32 | (defn clear-all-methods! [] (reset! lifecycle->method {})) 33 | 34 | (def cache (atom {})) 35 | 36 | (defn cached [q] (if-some [r (get-in @cache [(lifecycle q) q])] 37 | (do (trace/merge-trace! {:tags {:cached? true 38 | :reaction (reagent-id r)}}) 39 | r) 40 | (trace/merge-trace! {:tags {:cached? false}}))) 41 | 42 | (defn cache! [q r] (swap! cache assoc-in [(lifecycle q) q] r) r) 43 | 44 | (defn clear! 45 | ([] (reset! cache {})) 46 | ([q] (clear! q (lifecycle q))) 47 | ([q strat] (swap! cache update strat dissoc q))) 48 | 49 | (defn handle [q] 50 | (let [handler (get-handler :sub (id q))] 51 | (if-not (nil? handler) 52 | (handler app-db q) 53 | (do (trace/merge-trace! {:error true}) 54 | (console :error 55 | "re-frame: no subscription handler registered for: " 56 | (id q) 57 | ". Returning a nil subscription."))))) 58 | 59 | (defn query? [q] 60 | (some? (and (id q) 61 | (lifecycle q)))) 62 | -------------------------------------------------------------------------------- /src/re_frame/register/alpha.cljc: -------------------------------------------------------------------------------- 1 | (ns re-frame.register.alpha) 2 | 3 | (defmulti reg (fn [kind & _] kind)) 4 | 5 | (def lifecycle->method (atom {})) 6 | -------------------------------------------------------------------------------- /src/re_frame/registrar.cljc: -------------------------------------------------------------------------------- 1 | (ns re-frame.registrar 2 | "In many places, re-frame asks you to associate an `id` (keyword) 3 | with a `handler` (function). This namespace contains the 4 | central registry of such associations." 5 | (:require [re-frame.interop :refer [debug-enabled?]] 6 | [re-frame.loggers :refer [console]] 7 | [re-frame.settings :as settings])) 8 | 9 | ;; kinds of handlers 10 | (def kinds #{:event :fx :cofx :sub :error}) 11 | 12 | ;; This atom contains a register of all handlers. 13 | ;; Contains a two layer map, keyed first by `kind` (of handler), and then `id` of handler. 14 | ;; Leaf nodes are handlers. 15 | (def kind->id->handler (atom {})) 16 | 17 | (defn get-handler 18 | 19 | ([kind] 20 | (get @kind->id->handler kind)) 21 | 22 | ([kind id] 23 | (-> (get @kind->id->handler kind) 24 | (get id))) 25 | 26 | ([kind id required?] 27 | (let [handler (get-handler kind id)] 28 | (when debug-enabled? ;; This is in a separate `when` so Closure DCE can run ... 29 | (when (and required? (nil? handler)) ;; ...otherwise you'd need to type-hint the `and` with a ^boolean for DCE. 30 | (console :error "re-frame: no" (str kind) "handler registered for:" id))) 31 | handler))) 32 | 33 | (defn register-handler 34 | [kind id handler-fn] 35 | (when debug-enabled? ;; This is in a separate when so Closure DCE can run 36 | (when (and (not (settings/loaded?)) (get-handler kind id false)) 37 | (console :warn "re-frame: overwriting" (str kind) "handler for:" id))) ;; allow it, but warn. Happens on figwheel reloads. 38 | (swap! kind->id->handler assoc-in [kind id] handler-fn) 39 | handler-fn) ;; note: returns the just registered handler 40 | 41 | (defn clear-handlers 42 | ([] ;; clear all kinds 43 | (reset! kind->id->handler {})) 44 | 45 | ([kind] ;; clear all handlers for this kind 46 | (assert (kinds kind)) 47 | (swap! kind->id->handler dissoc kind)) 48 | 49 | ([kind id] ;; clear a single handler for a kind 50 | (assert (kinds kind)) 51 | (if (get-handler kind id) 52 | (swap! kind->id->handler update-in [kind] dissoc id) 53 | (console :warn "re-frame: can't clear" (str kind) "handler for" (str id ". Handler not found."))))) 54 | -------------------------------------------------------------------------------- /src/re_frame/settings.cljc: -------------------------------------------------------------------------------- 1 | (ns re-frame.settings 2 | (:require 3 | [re-frame.interop :as interop] 4 | [re-frame.loggers :refer [console]])) 5 | 6 | (def defaults 7 | {:loaded? false 8 | :global-interceptors interop/empty-queue}) 9 | 10 | (def store 11 | (atom defaults)) 12 | 13 | (interop/on-load 14 | #(swap! store (fn [m] (assoc m :loaded? true)))) 15 | 16 | (defn loaded? 17 | [] 18 | (:loaded? @store)) 19 | 20 | (defn -replace-global-interceptor 21 | [global-interceptors interceptor] 22 | (reduce 23 | (fn [ret existing-interceptor] 24 | (if (= (:id interceptor) 25 | (:id existing-interceptor)) 26 | (do 27 | (when interop/debug-enabled? 28 | (when (not (loaded?)) 29 | (console :warn "re-frame: replacing duplicate global interceptor id: " (:id interceptor)))) 30 | (conj ret interceptor)) 31 | (conj ret existing-interceptor))) 32 | interop/empty-queue 33 | global-interceptors)) 34 | 35 | (defn reg-global-interceptor 36 | [{:keys [id] :as interceptor}] 37 | (swap! store update :global-interceptors 38 | (fn [global-interceptors] 39 | (let [ids (map :id global-interceptors)] 40 | (if (some #{id} ids) 41 | ;; If the id already exists we replace it in-place to maintain the ordering of 42 | ;; global interceptors esp during hot-code reloading in development. 43 | (-replace-global-interceptor global-interceptors interceptor) 44 | (conj global-interceptors interceptor)))))) 45 | 46 | (defn get-global-interceptors 47 | [] 48 | (:global-interceptors @store)) 49 | 50 | (defn clear-global-interceptors 51 | ([] 52 | (swap! store assoc :global-interceptors interop/empty-queue)) 53 | ([id] 54 | (swap! store update :global-interceptors 55 | (fn [global-interceptors] 56 | (into interop/empty-queue (remove #(= id (:id %)) global-interceptors)))))) 57 | -------------------------------------------------------------------------------- /src/re_frame/subs/alpha.cljc: -------------------------------------------------------------------------------- 1 | (ns re-frame.subs.alpha 2 | (:require 3 | [re-frame.subs :refer [deref-input-signals sugar warn-when-not-reactive]] 4 | [re-frame.registrar :refer [register-handler]] 5 | [re-frame.register.alpha :refer [reg lifecycle->method]] 6 | [re-frame.interop :refer [add-on-dispose! make-reaction reactive? reagent-id ratom]] 7 | [re-frame.query.alpha :as q] 8 | [re-frame :as-alias rf] 9 | [re-frame.trace :as trace :include-macros true] 10 | [re-frame.flow.alpha :as flow])) 11 | 12 | (defmethod reg :sub-lifecycle [_ k f] 13 | (swap! lifecycle->method assoc 14 | k 15 | (fn [q] 16 | (trace/with-trace {:operation (q/id q) 17 | :op-type :sub/create 18 | :tags {:query q}} 19 | (f q))))) 20 | 21 | (defn sub 22 | ([q] 23 | (if (keyword? q) 24 | (sub q {}) 25 | (let [md (q/method q)] 26 | (cond (map? q) (md q) 27 | (vector? q) (md {::rf/q (q/id q) 28 | ::rf/lifecycle (q/lifecycle q) 29 | ::rf/query-v q}))))) 30 | ([id q] 31 | (sub (assoc q ::rf/q id)))) 32 | 33 | (defmethod reg :sub [kind id & args] 34 | (let [[inputs-fn computation-fn] (apply sugar id sub q/query? args)] 35 | (register-handler 36 | kind 37 | id 38 | (fn subs-handler-fn [_ q] 39 | (let [subscriptions (inputs-fn q nil) 40 | rid (atom nil) 41 | r (make-reaction 42 | #(trace/with-trace {:operation (q/id q) 43 | :op-type :sub/run 44 | :tags {:query q 45 | :reaction @rid}} 46 | (let [subscription (computation-fn 47 | (deref-input-signals subscriptions id) 48 | q)] 49 | (trace/merge-trace! {:tags {:value subscription}}) 50 | subscription)))] 51 | (reset! rid (reagent-id r)) 52 | r))))) 53 | 54 | (defmethod reg :legacy-sub [_ id & args] 55 | (let [[inputs-fn computation-fn] (apply sugar id sub q/query? args)] 56 | (register-handler 57 | :sub 58 | id 59 | (fn subs-handler-fn [_ q] 60 | (let [subscriptions (inputs-fn q nil) 61 | rid (atom nil) 62 | r (make-reaction 63 | #(trace/with-trace {:operation (q/id q) 64 | :op-type :sub/run 65 | :tags {:query q 66 | :reaction @rid}} 67 | (let [q (if (map? q) 68 | (-> (or (::rf/query-v q) [(q/id q)]) 69 | (vary-meta assoc ::rf/lifecycle (q/lifecycle q))) 70 | q) 71 | subscription (computation-fn 72 | (deref-input-signals subscriptions id) 73 | q)] 74 | (trace/merge-trace! {:tags {:value subscription}}) 75 | subscription)))] 76 | (reset! rid (reagent-id r)) 77 | r))))) 78 | 79 | (defn sub-reactive [q] 80 | (warn-when-not-reactive) 81 | (or (q/cached q) 82 | (let [md (q/lifecycle q) 83 | r (q/handle q)] 84 | (add-on-dispose! r #(q/clear! q md)) 85 | (q/cache! q r)))) 86 | 87 | (reg :sub-lifecycle :reactive sub-reactive) 88 | 89 | (defn sub-safe [q] 90 | (if (reactive?) 91 | (sub-reactive q) 92 | (or (q/cached q) 93 | (q/handle q)))) 94 | 95 | (reg :sub-lifecycle :safe sub-safe) 96 | (reg :sub-lifecycle :default sub-safe) 97 | 98 | (defn sub-forever [q] 99 | (or (q/cached q) 100 | (q/cache! q (q/handle q)))) 101 | 102 | (reg :sub-lifecycle :forever sub-forever) 103 | 104 | (def nil-ref (ratom nil)) 105 | 106 | (defn sub-flow [q] 107 | (or (some-> (:id (or (second (::rf/query-v q)) q)) 108 | flow/lookup meta :re-frame.flow.alpha/ref) 109 | nil-ref)) 110 | 111 | (reg :sub-lifecycle :flow sub-flow) 112 | -------------------------------------------------------------------------------- /src/re_frame/utils.cljc: -------------------------------------------------------------------------------- 1 | (ns re-frame.utils 2 | (:require 3 | [re-frame.loggers :refer [console]] 4 | [re-frame.interop :as interop])) 5 | 6 | (defn dissoc-in 7 | "Dissociates an entry from a nested associative structure returning a new 8 | nested structure. keys is a sequence of keys. Any empty maps that result 9 | will not be present in the new structure. 10 | The key thing is that 'm' remains identical? to itself if the path was never present" 11 | [m [k & ks :as keys]] 12 | (if ks 13 | (if-let [nextmap (get m k)] 14 | (let [newmap (dissoc-in nextmap ks)] 15 | (if (seq newmap) 16 | (assoc m k newmap) 17 | (dissoc m k))) 18 | m) 19 | (dissoc m k))) 20 | 21 | (defn first-in-vector 22 | [v] 23 | (if (vector? v) 24 | (first v) 25 | (console :error "re-frame: expected a vector, but got:" v))) 26 | 27 | (defn apply-kw 28 | "Like apply, but f takes keyword arguments and the last argument is 29 | not a seq but a map with the arguments for f" 30 | [f & args] 31 | {:pre [(map? (last args))]} 32 | (apply f (apply concat 33 | (butlast args) (last args)))) 34 | 35 | (defn map-vals [f m] 36 | (into {} (map (fn [[k v]] [k (f v)])) m)) 37 | 38 | (defn find-cycle [graph visited node] 39 | (loop [stack [node] 40 | path []] 41 | (let [current (peek stack)] 42 | (if (some #{current} path) 43 | (conj (take-while #(not= % current) (reverse path)) current) 44 | (if-let [neighbors (seq (get graph current))] 45 | (recur (into stack (disj (set neighbors) visited)) 46 | (conj path current)) 47 | (recur (pop stack) path)))))) 48 | 49 | (defn topsort-kahn [graph] 50 | (let [in-degree (reduce (fn [acc [node neighbors]] 51 | (reduce (fn [a neighbor] 52 | (update a neighbor inc)) 53 | acc neighbors)) 54 | {} graph) 55 | ks (keys graph)] 56 | (loop [q (filter #(zero? (get in-degree % 0)) ks) 57 | sorted interop/empty-queue 58 | in-degree in-degree] 59 | (cond 60 | (seq q) 61 | (let [current (first q) 62 | neighbors (get graph current []) 63 | updated-in-degree (reduce (fn [acc neighbor] 64 | (update acc neighbor dec)) 65 | in-degree neighbors) 66 | new-q (concat (rest q) 67 | (filter #(= 0 (get updated-in-degree %)) neighbors))] 68 | (recur new-q (conj sorted current) updated-in-degree)) 69 | (= (count sorted) (count ks)) 70 | sorted 71 | :else 72 | (let [unvisited (remove (set sorted) ks) 73 | cycle (some #(find-cycle graph (set sorted) %) unvisited)] 74 | (throw (#?(:clj Exception. :cljs js/Error.) 75 | (str "Graph has a cycle: " cycle)))))))) 76 | 77 | (defn remove-orphans [graph] 78 | (map-vals (partial filterv (set (keys graph))) graph)) 79 | 80 | (defn safe-update-in [m path f & args] 81 | (if (empty? path) 82 | (apply f m args) 83 | (apply update-in m path f args))) 84 | 85 | (defn deep-dissoc 86 | "Dissoces the map entry at the path, then recurs through the ancestors, 87 | dissocing each ancestor until one is found with a descendent outside the path. 88 | 89 | ``` 90 | (deep-dissoc {:a {:b {:c {:d 1}}}} 91 | [:a :b :c :d]) 92 | ``` 93 | 94 | This yields an empty map, since each node has a sole descendant. 95 | 96 | ``` 97 | (deep-dissoc {:a {:x 2 :b {:c {:d 1}}}} 98 | [:a :b :c :d]) 99 | ``` 100 | 101 | This yields `{:a {:x 2}}`, since `:a` has a descendent `:x` outside the path. 102 | " 103 | [m path] 104 | (if 105 | (empty? path) m 106 | (let [new-data (safe-update-in m (pop path) dissoc (peek path))] 107 | (if-not (empty? (get-in new-data (pop path))) 108 | new-data 109 | (recur new-data (pop path)))))) 110 | -------------------------------------------------------------------------------- /test/re_frame/event_test.cljs: -------------------------------------------------------------------------------- 1 | (ns re-frame.event-test 2 | (:require [cljs.test :refer-macros [is deftest]] 3 | [re-frame.db :as db] 4 | [re-frame.core :as re-frame])) 5 | 6 | ;=====test basic subscriptions ====== 7 | 8 | ;; disabled as it doesn't really test anything 9 | #_(deftest test-event-def 10 | "tests that an error thrown generates an informational warning" 11 | (re-frame/clear-all-events!) 12 | 13 | (re-frame/reg-event-db 14 | :test-event 15 | (fn [db [event-kw stack]] 16 | (throw (js/Error. "thrown in handler")) 17 | db)) 18 | 19 | (defn test-fn1 20 | [] 21 | (re-frame/dispatch [:test-event])) 22 | 23 | (defn test-fn2 24 | [] 25 | (test-fn1)) 26 | 27 | (test-fn2)) 28 | 29 | 30 | -------------------------------------------------------------------------------- /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! {:warn 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 :refer [make-restore-fn reg-sub subscribe]] 4 | [re-frame.subs :as subs])) 5 | 6 | ;; TODO: future tests in this area could check DB state and registrations are being correctly restored. 7 | 8 | (use-fixtures :each {:before subs/clear-all-handlers!}) 9 | 10 | (defn one? [x] (= 1 x)) 11 | (defn two? [x] (= 2 x)) 12 | 13 | (defn register-test-subs [] 14 | (reg-sub 15 | :test-sub 16 | (fn [db ev] 17 | (:test-sub db))) 18 | 19 | (reg-sub 20 | :test-sub2 21 | (fn [db ev] 22 | (:test-sub2 db)))) 23 | 24 | (deftest make-restore-fn-test 25 | (testing "no existing subs, then making one subscription" 26 | (register-test-subs) 27 | (let [original-subs @subs/query->reaction 28 | restore-fn (make-restore-fn)] 29 | (is (zero? (count original-subs))) 30 | @(subscribe [:test-sub]) 31 | (is (one? (count @subs/query->reaction))) 32 | (is (contains? @subs/query->reaction [[:test-sub] []])) 33 | (restore-fn) 34 | (is (zero? (count @subs/query->reaction)))))) 35 | 36 | (deftest make-restore-fn-test2 37 | (testing "existing subs, making more subscriptions" 38 | (register-test-subs) 39 | @(subscribe [:test-sub]) 40 | (let [original-subs @subs/query->reaction 41 | restore-fn (make-restore-fn)] 42 | (is (one? (count original-subs))) 43 | @(subscribe [:test-sub2]) 44 | (is (contains? @subs/query->reaction [[:test-sub2] []])) 45 | (is (two? (count @subs/query->reaction))) 46 | (restore-fn) 47 | (is (not (contains? @subs/query->reaction [[:test-sub2] []]))) 48 | (is (one? (count @subs/query->reaction)))))) 49 | 50 | (deftest make-restore-fn-test3 51 | (testing "existing subs, making more subscriptions with different params on same subscriptions" 52 | (register-test-subs) 53 | @(subscribe [:test-sub]) 54 | (let [original-subs @subs/query->reaction 55 | restore-fn (make-restore-fn)] 56 | (is (one? (count original-subs))) 57 | @(subscribe [:test-sub :extra :params]) 58 | (is (two? (count @subs/query->reaction))) 59 | (restore-fn) 60 | (is (one? (count @subs/query->reaction)))))) 61 | 62 | (deftest nested-restores 63 | (testing "running nested restores" 64 | (register-test-subs) 65 | (let [restore-fn-1 (make-restore-fn) 66 | _ @(subscribe [:test-sub]) 67 | _ (is (one? (count @subs/query->reaction))) 68 | restore-fn-2 (make-restore-fn)] 69 | @(subscribe [:test-sub2]) 70 | (is (two? (count @subs/query->reaction))) 71 | (restore-fn-2) 72 | (is (one? (count @subs/query->reaction))) 73 | (restore-fn-1) 74 | (is (zero? (count @subs/query->reaction)))))) 75 | -------------------------------------------------------------------------------- /test/re_frame/router_test.clj: -------------------------------------------------------------------------------- 1 | (ns re-frame.router-test 2 | (:require [clojure.test :refer :all] 3 | [re-frame.core :as rf] 4 | [re-frame.db :as db])) 5 | 6 | (defn fixture-re-frame 7 | [f] 8 | (let [restore-re-frame (re-frame.core/make-restore-fn)] 9 | (f) 10 | (restore-re-frame))) 11 | 12 | (use-fixtures :each fixture-re-frame) 13 | 14 | (rf/reg-event-db 15 | ::test 16 | (fn [db [_ i]] 17 | (update db ::test (fnil conj []) i))) 18 | 19 | (rf/reg-fx 20 | ::promise 21 | (fn [{:keys [p val]}] 22 | (deliver p val))) 23 | 24 | (rf/reg-event-fx 25 | ::sentinel 26 | (fn [cofx [_ p val]] 27 | {::promise {:p p :val val}})) 28 | 29 | (deftest dispatching-race-condition-469-test 30 | ;; Checks for day8/re-frame#469 31 | (let [p (promise)] 32 | (is (nil? (dotimes [i 1000] 33 | (rf/dispatch [::test i])))) 34 | (is (nil? (rf/dispatch [::sentinel p ::done]))) 35 | (let [val (deref p 1000 ::timed-out)] 36 | (is (= ::done val))) 37 | (is (= (::test @db/app-db) 38 | (range 1000))))) 39 | -------------------------------------------------------------------------------- /test/re_frame/subs_test.clj: -------------------------------------------------------------------------------- 1 | (ns re-frame.subs-test 2 | (:require [clojure.test :refer :all] 3 | [re-frame.subs :as subs] 4 | [re-frame.db :as db])) 5 | 6 | (defn fixture-re-frame 7 | [f] 8 | (let [restore-re-frame (re-frame.core/make-restore-fn)] 9 | (f) 10 | (restore-re-frame))) 11 | 12 | (use-fixtures :each fixture-re-frame) 13 | 14 | (deftest test-reg-sub-clj-repl 15 | (subs/reg-sub 16 | :a-sub 17 | (fn [db _] (:a db))) 18 | 19 | (subs/reg-sub 20 | :b-sub 21 | (fn [db _] (:b db))) 22 | 23 | (subs/reg-sub 24 | :a-b-sub 25 | (fn [_ _] 26 | [(subs/subscribe [:a-sub]) 27 | (subs/subscribe [:b-sub])]) 28 | (fn [[a b] _] 29 | {:a a :b b})) 30 | 31 | (let [test-sub (subs/subscribe [:a-b-sub])] 32 | (reset! db/app-db {:a 1 :b 2}) 33 | (is (= {:a 1 :b 2} @test-sub)) 34 | (swap! db/app-db assoc :b 3) 35 | (is (= {:a 1 :b 3} @test-sub)))) 36 | -------------------------------------------------------------------------------- /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 | (defn ^:export run-html-tests [] 20 | (cljs-test/run-tests 21 | 're-frame.interceptor-test 22 | 're-frame.subs-test 23 | 're-frame.fx-test 24 | 're-frame.trace-test 25 | 're-frame.restore-test)) 26 | 27 | ;; ---- KARMA ----------------------------------------------------------------- 28 | 29 | (defn ^:export run-karma [karma] 30 | (karma/run-tests 31 | karma 32 | 're-frame.interceptor-test 33 | 're-frame.subs-test 34 | 're-frame.fx-test 35 | 're-frame.trace-test 36 | 're-frame.restore-test)) 37 | -------------------------------------------------------------------------------- /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/re_frame/utils_test.cljs: -------------------------------------------------------------------------------- 1 | (ns re-frame.utils-test 2 | (:require 3 | [cljs.test :refer [is deftest async use-fixtures testing]] 4 | [re-frame.utils :as u])) 5 | 6 | (deftest topsort 7 | (testing "acyclic graph" 8 | (let [acyclic-graph {:A [:B :C] 9 | :B [:D] 10 | :C [:D] 11 | :D []}] 12 | (is (= '(:A :B :C :D) (u/topsort-kahn acyclic-graph))))) 13 | (testing "cyclic graph" 14 | 15 | (let [cyclic-graph {:A [:B] 16 | :B [:C] 17 | :C [:A]} 18 | e (atom nil)] 19 | (is (thrown-with-msg? js/Error 20 | #"Graph has a cycle:" 21 | (u/topsort-kahn cyclic-graph))))) 22 | (testing "orphaned edges" 23 | (is (= {:A [:B] :B []} (u/remove-orphans {:A [:B :C] 24 | :B []}))))) 25 | 26 | (testing "deep dissoc" 27 | (is (= {} (u/deep-dissoc {:a {:b {:c {:d 1}}}} 28 | [:a :b :c :d]))) 29 | (is (= {:a {:x 2}} (u/deep-dissoc {:a {:x 2 :b {:c {:d 1}}}} 30 | [:a :b :c :d])))) 31 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------