├── .clj-kondo ├── config.edn └── funcool │ └── promesa │ └── config.edn ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── issue_template.md └── workflows │ └── clojure.yml ├── .gitignore ├── .lsp └── config.edn ├── .nvmrc ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── ROADMAP.md ├── SECURITY.md ├── bb.edn ├── build-docs.sh ├── build-example-site.sh ├── codecov.yml ├── demo ├── reagentdemo │ ├── common.cljs │ ├── core.cljs │ ├── dev.cljs │ ├── intro.cljs │ ├── news.cljs │ ├── news │ │ ├── anyargs.cljs │ │ ├── async.cljs │ │ ├── binaryclock.cljs │ │ ├── clockpost.cljs │ │ ├── news050.cljs │ │ ├── news051.cljs │ │ ├── news060.cljs │ │ ├── news060rc.cljs │ │ ├── news060release.cljs │ │ ├── news061.cljs │ │ └── undodemo.cljs │ ├── prod.cljs │ ├── syntax.clj │ └── syntax.cljs └── sitetools │ └── core.cljs ├── deps.edn ├── doc ├── 0.8-upgrade.md ├── BatchingAndTiming.md ├── ControlledInputs.md ├── CreatingReagentComponents.md ├── FAQ │ ├── CljsjsReactProblems.md │ ├── ComponentNotRerendering.md │ ├── ForcingComponentRecreation.md │ ├── HtmlEntities.md │ ├── MyAttributesAreMissing.md │ ├── UsingAnEntity.md │ ├── UsingRefs.md │ └── dangerouslySetInnerHTML.md ├── InteropWithReact.md ├── ManagingState.md ├── README.md ├── ReactFeatures.md ├── ReactRenderers.md ├── ReagentCompiler.md ├── Security.md ├── ServerSideRendering.md ├── UsingHiccupToDescribeHTML.md ├── UsingSquareBracketsInsteadOfParens.md ├── WhenDoComponentsUpdate.md ├── benchmark.png ├── cljdoc.edn ├── development.md └── examples │ ├── material-ui.md │ └── smooth-ui.md ├── examples ├── functional-components-and-hooks │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── resources │ │ └── public │ │ │ ├── example.css │ │ │ └── index.html │ ├── shadow-cljs.edn │ └── src │ │ └── example │ │ └── core.cljs ├── geometry │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── resources │ │ └── public │ │ │ ├── example.css │ │ │ └── index.html │ ├── shadow-cljs.edn │ └── src │ │ └── geometry │ │ ├── components.cljs │ │ ├── core.cljs │ │ └── geometry.cljs ├── material-ui │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── resources │ │ └── public │ │ │ └── index.html │ ├── shadow-cljs.edn │ └── src │ │ └── example │ │ └── core.cljs ├── react-context │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── resources │ │ └── public │ │ │ └── index.html │ ├── shadow-cljs.edn │ └── src │ │ └── example │ │ └── core.cljs ├── react-mde │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── resources │ │ └── public │ │ │ ├── index.html │ │ │ └── react-mds-all.css │ ├── shadow-cljs.edn │ └── src │ │ └── example │ │ └── core.cljs ├── react-sortable-hoc │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── resources │ │ └── public │ │ │ └── index.html │ ├── shadow-cljs.edn │ └── src │ │ └── example │ │ └── core.cljs ├── react-transition-group │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── resources │ │ └── public │ │ │ ├── fade.css │ │ │ └── index.html │ ├── shadow-cljs.edn │ └── src │ │ └── example │ │ └── core.cljs ├── simple │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── resources │ │ └── public │ │ │ ├── example.css │ │ │ └── index.html │ ├── shadow-cljs.edn │ └── src │ │ └── simpleexample │ │ └── core.cljs └── todomvc │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── resources │ └── public │ │ ├── index.html │ │ └── todos.css │ ├── shadow-cljs.edn │ └── src │ └── todomvc │ └── core.cljs ├── lib └── modules.js ├── logo ├── logo-text.png ├── logo-text.svg ├── logo.png └── logo.svg ├── outsite └── public │ └── README.md ├── package-lock.json ├── package.json ├── prepare-tests.sh ├── prerender └── sitetools │ └── prerender.cljs ├── project.clj ├── run-tests.sh ├── shadow-cljs.edn ├── site └── public │ ├── css │ ├── examples.css │ └── main.css │ └── index.html ├── src ├── clj-kondo │ └── clj-kondo.exports │ │ └── reagent │ │ └── reagent │ │ └── config.edn └── reagent │ ├── core.clj │ ├── core.cljs │ ├── debug.clj │ ├── debug.cljs │ ├── dom.cljs │ ├── dom │ ├── client.cljs │ └── server.cljs │ ├── impl │ ├── batching.cljs │ ├── component.cljs │ ├── input.cljs │ ├── protocols.cljs │ ├── template.cljs │ └── util.cljs │ ├── interop.clj │ ├── interop.cljs │ ├── ratom.clj │ └── ratom.cljs ├── test-environments ├── browser-cljsjs-prod │ └── test.sh ├── browser-cljsjs │ └── test.sh ├── browser-npm-prod │ └── test.sh ├── browser-npm │ └── test.sh ├── bundle-adv │ ├── README.md │ ├── build.edn │ ├── build.sh │ ├── karma.conf.js │ ├── karma.edn │ ├── test.sh │ └── webpack.config.js ├── bundle │ ├── build.edn │ ├── karma.conf.js │ ├── test.sh │ ├── webpack.config.js │ └── workaround.js ├── node-cljsjs │ └── test_disabled.sh ├── node-npm │ └── test.sh └── shadow-cljs-prod │ ├── karma.conf.js │ └── test.sh └── test ├── reagent └── impl │ ├── template_test.cljs │ └── util_test.cljs ├── reagenttest ├── performancetest.cljs ├── runtests.cljs ├── testcursor.cljs ├── testratom.cljs ├── testratomasync.cljs ├── testreagent.cljs ├── testtrack.cljs ├── testwithlet.cljs ├── testwrap.cljs ├── utils.clj └── utils.cljs └── runners └── karma.conf.js /.clj-kondo/config.edn: -------------------------------------------------------------------------------- 1 | {:lint-as {reagent.core/with-let clojure.core/let 2 | reagenttest.utils/deftest clojure.test/deftest 3 | reagenttest.utils/with-render clojure.core/let} 4 | :linters {:unused-binding {:level :off} 5 | :missing-else-branch {:level :off} 6 | :unused-referred-var {:exclude {cljs.test [deftest testing is]}} 7 | ;; Example namespaces use clashing names now 8 | :redefined-var {:level :off}}} 9 | -------------------------------------------------------------------------------- /.clj-kondo/funcool/promesa/config.edn: -------------------------------------------------------------------------------- 1 | {:lint-as {promesa.core/-> clojure.core/-> 2 | promesa.core/->> clojure.core/->> 3 | promesa.core/as-> clojure.core/as-> 4 | promesa.core/let clojure.core/let 5 | promesa.core/plet clojure.core/let 6 | promesa.core/loop clojure.core/loop 7 | promesa.core/recur clojure.core/recur 8 | promesa.core/with-redefs clojure.core/with-redefs 9 | promesa.core/doseq clojure.core/doseq}} 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | Please check existing issues first if they have already been reported. Check [controlled inputs issue](https://github.com/reagent-project/reagent/issues/619), especially for all problems related to the controlled inputs workaround. 11 | 12 | **Describe the bug** 13 | A clear and concise description of what the bug is. 14 | 15 | **To Reproduce** 16 | Steps to reproduce the behavior: 17 | 18 | **Expected behavior** 19 | A clear and concise description of what you expected to happen. 20 | 21 | **Environment:** 22 | - Reagent version 23 | - ClojureScript and build tooling version 24 | - Browser and version 25 | 26 | **Additional context** 27 | Please feel free to add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | You can open an issue if you have a clear idea of how it would work. If you have less idea of how the idea would work, consider using [discussions](https://github.com/reagent-project/reagent/discussions) to get feedback if the idea is feasible to implement. 11 | 12 | **Is your feature request related to a problem? Please describe.** 13 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 14 | 15 | **Describe the solution you'd like** 16 | A clear and concise description of what you want to happen. 17 | 18 | **Describe alternatives you've considered** 19 | A clear and concise description of any alternative solutions or features you've considered. 20 | 21 | **Additional context** 22 | Add any other context or screenshots about the feature request here. 23 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | No support questions in here please. This is a place for bug reports and feature requests only. 2 | 3 | Support requests and usage questions should go to the #reagent channel in [Clojure Slack](http://clojurians.net/) or the [ClojureScript mailing list](https://groups.google.com/forum/#!forum/clojurescript). 4 | -------------------------------------------------------------------------------- /.github/workflows/clojure.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Run tests 3 | 4 | on: [push, pull_request] 5 | 6 | jobs: 7 | test: 8 | strategy: 9 | matrix: 10 | env: 11 | - browser-cljsjs 12 | - browser-cljsjs-prod 13 | - browser-npm 14 | - browser-npm-prod 15 | - bundle 16 | - bundle-adv 17 | # - node-cljsjs 18 | - node-npm 19 | - shadow-cljs-prod 20 | 21 | name: Test ${{ matrix.env }} 22 | 23 | runs-on: ubuntu-latest 24 | env: 25 | ENV: ${{ matrix.env }} 26 | 27 | steps: 28 | - uses: actions/checkout@v4 29 | - name: Setup Java 30 | uses: actions/setup-java@v4 31 | with: 32 | distribution: 'adopt' 33 | java-version: 17 34 | - name: Setup Clojure 35 | uses: DeLaGuardo/setup-clojure@master 36 | with: 37 | lein: 2.9.10 38 | cli: latest 39 | - name: Setup Node.js 40 | uses: actions/setup-node@v4 41 | with: 42 | node-version: lts/hydrogen 43 | cache: 'npm' 44 | 45 | # setup-java cache only looks at pom.xml for the key 46 | - name: Cache m2 47 | uses: actions/cache@v4 48 | with: 49 | path: ~/.m2 50 | key: ${{ runner.os }}-m2-${{ hashFiles('**/project.clj') }} 51 | restore-keys: | 52 | ${{ runner.os }}-m2- 53 | 54 | - name: Install node deps 55 | run: npm ci 56 | 57 | - name: Run tests 58 | run: ./test-environments/${{ matrix.env }}/test.sh 59 | 60 | - uses: codecov/codecov-action@v5 61 | with: 62 | env_vars: ENV 63 | 64 | update-site: 65 | name: Update site 66 | runs-on: ubuntu-latest 67 | if: ${{ github.ref == 'refs/heads/master' }} 68 | needs: test 69 | environment: build-site 70 | steps: 71 | - uses: actions/checkout@v4 72 | - name: Setup Java 73 | uses: actions/setup-java@v4 74 | with: 75 | distribution: 'adopt' 76 | java-version: 17 77 | - name: Setup Clojure 78 | uses: DeLaGuardo/setup-clojure@master 79 | with: 80 | lein: 2.9.8 81 | cli: latest 82 | - name: Setup Node.js 83 | uses: actions/setup-node@v4 84 | with: 85 | node-version: lts/hydrogen 86 | cache: 'npm' 87 | 88 | # setup-java cache only looks at pom.xml for the key 89 | - name: Cache m2 90 | uses: actions/cache@v4 91 | with: 92 | path: ~/.m2 93 | key: ${{ runner.os }}-m2-${{ hashFiles('**/project.clj') }} 94 | restore-keys: | 95 | ${{ runner.os }}-m2- 96 | 97 | - name: Install node deps 98 | run: npm ci 99 | 100 | - name: Build site 101 | run: ./build-example-site.sh 102 | env: 103 | SITE_TOKEN: ${{ secrets.SITE_TOKEN }} 104 | 105 | update-tagged-docs: 106 | name: Update docs 107 | runs-on: ubuntu-latest 108 | if: ${{ startsWith(github.ref, 'refs/tags/') }} 109 | needs: test 110 | steps: 111 | - uses: actions/checkout@v4 112 | - name: Setup Java 113 | uses: actions/setup-java@v4 114 | with: 115 | distribution: 'zulu' 116 | java-version: 8 117 | - name: Setup Clojure 118 | uses: DeLaGuardo/setup-clojure@master 119 | with: 120 | lein: 2.9.8 121 | cli: latest 122 | 123 | # setup-java cache only looks at pom.xml for the key 124 | - name: Cache m2 125 | uses: actions/cache@v4 126 | with: 127 | path: ~/.m2 128 | key: ${{ runner.os }}-m2-${{ hashFiles('**/project.clj') }} 129 | restore-keys: | 130 | ${{ runner.os }}-m2- 131 | 132 | - name: Build docs 133 | run: ./build-docs.sh 134 | # Fine grained personal access token, 366 days expiry, only contents rw permission to the site repo 135 | env: 136 | SITE_TOKEN: ${{ secrets.SITE_TOKEN }} 137 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | pom.xml 3 | .lein-repl-history 4 | pom.xml.asc 5 | .nrepl-port 6 | demo/empty.cljs 7 | out 8 | tmp 9 | figwheel_server.log 10 | 11 | reagent.iml 12 | .idea 13 | .lein-failures 14 | 15 | node_modules 16 | junit 17 | .shadow-cljs 18 | coverage/ 19 | 20 | .clj-kondo/.cache 21 | .cpcache/ 22 | .lsp/.cache 23 | .nrepl.edn 24 | -------------------------------------------------------------------------------- /.lsp/config.edn: -------------------------------------------------------------------------------- 1 | {:clean {:automatically-after-ns-refactor true 2 | :ns-inner-blocks-indentation :same-line 3 | :sort {:require :lexicographically}} 4 | :cljfmt {:function-arguments-indentation :cursive}} 5 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/hydrogen 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Reagent 2 | 3 | :+1::tada: First off, thanks for taking the time to contribute! :tada::+1: 4 | 5 | The following is a set of guidelines for contributing to Reagent which is hosted on [Github](fram://github.com/reagent-project/reagent). 6 | These are just guidelines, not rules. Use your best judgment and feel free to propose changes to this document in a pull request. 7 | 8 | ## Support questions 9 | 10 | **The Github issues for Reagent are for bug reports and feature requests. Support requests and usage questions should go to the [Clojure Slack channel](http://clojurians.net), [Clojureverse discussion board](https://clojureverse.org/), the [ClojureScript mailing list](https://groups.google.com/forum/#!forum/clojurescript), or the [Reagent mailing list](https://groups.google.com/forum/#!forum/reagent-project).** 11 | 12 | ## Creating issues for bugs 13 | 14 | Check if the issue has already been reported. If possible provide: 15 | 16 | * Version of Reagent being used 17 | * Minimal reproduction steps 18 | 19 | ## Creating issues for features 20 | 21 | Use your best judgement on what is needed here. 22 | 23 | ## Pull requests 24 | 25 | **Create pull requests to the master branch**. 26 | 27 | Check [development docs](./doc/development.md) 28 | 29 | ## Pull requests for bugs 30 | 31 | If possible provide: 32 | 33 | * Code that fixes the bug 34 | * Failing tests which pass with the new changes 35 | * Improvements to documentation to make it less likely that others will run into issues (if relevant). 36 | * Add the change to the Unreleased section of [CHANGELOG.md](CHANGELOG.md) 37 | 38 | ## Pull requests for features 39 | 40 | If possible provide: 41 | 42 | * Code that implements the new feature 43 | * Tests to cover the new feature including all of the code paths 44 | * Docstrings for functions 45 | * Documentation examples 46 | * Add the change to the Unreleased section of [CHANGELOG.md](CHANGELOG.md) 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2013-2017 Dan Holmsand 4 | Copyright (c) 2017 Reagent contributors 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | # Maintainer's roadmap 2 | 3 | - Replace Doo with Kaocha-cljs2 or other more modern cljs test runner 4 | - Make functional components the default 5 | - Build a new site 6 | - Make Reagent React 18 and Concurrent Mode compatible: [#554](https://github.com/reagent-project/reagent/pull/554) 7 | - New API to use regular atoms as ratoms: [#549](https://github.com/reagent-project/reagent/issues/546) 8 | - Add React hook for using Ratoms/reactions through a hook: [#545](https://github.com/reagent-project/reagent/issues/545) 9 | - Needs React 18 for `useSyncExternalStore` 10 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | If you have found an issue that causes a vulnerability and should be reported without making it public, you can contact [Juho Teperi](https://github.com/Deraen) over email or Clojurians Slack. 6 | -------------------------------------------------------------------------------- /bb.edn: -------------------------------------------------------------------------------- 1 | {:tasks {figwheel {:task (shell "lein figwheel")} 2 | shadow {:task (shell "npx shadow-cljs watch client")}}} 3 | -------------------------------------------------------------------------------- /build-docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | TAG=${GITHUB_REF/refs\/tags\//} 6 | 7 | if [[ -z $TAG ]]; then 8 | echo "Set TAG" 9 | exit 1 10 | fi 11 | 12 | lein codox 13 | 14 | rm -fr tmp 15 | if [[ -n $GITHUB_ACTOR ]]; then 16 | git config --global user.email "14146879+github-actions[bot]@users.noreply.github.com" 17 | git config --global user.name "github-actions[bot]" 18 | git clone "https://${GITHUB_ACTOR}:${SITE_TOKEN}@github.com/reagent-project/reagent-project.github.io.git" tmp 19 | else 20 | git clone git@github.com:reagent-project/reagent-project.github.io.git tmp 21 | fi 22 | 23 | mkdir -p "tmp/docs/$TAG/" 24 | cp -r target/doc/* "tmp/docs/$TAG/" 25 | 26 | cd tmp 27 | git add . 28 | git commit -m "Built docs from $TAG" 29 | git push 30 | rm -rf tmp 31 | -------------------------------------------------------------------------------- /build-example-site.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | SHA=$(git rev-parse HEAD) 6 | 7 | lein 'do' clean, cljsbuild once prod-npm, cljsbuild once prerender 8 | 9 | node target/cljsbuild/prerender/main.js target/cljsbuild/prod-npm/public/ 10 | 11 | lein codox 12 | 13 | rm -fr tmp 14 | if [[ -n $GITHUB_ACTOR ]]; then 15 | git config --global user.email "14146879+github-actions[bot]@users.noreply.github.com" 16 | git config --global user.name "github-actions[bot]" 17 | git clone "https://${GITHUB_ACTOR}:${SITE_TOKEN}@github.com/reagent-project/reagent-project.github.io.git" tmp 18 | else 19 | git clone git@github.com:reagent-project/reagent-project.github.io.git tmp 20 | fi 21 | 22 | # Remove everything to ensure old files are removed 23 | rm -fr tmp/* 24 | 25 | cp -r target/cljsbuild/prod-npm/public/* tmp/ 26 | cp -r target/prerender/public/* tmp/ 27 | mkdir -p tmp/docs/master/ 28 | cp -r target/doc/* tmp/docs/master/ 29 | 30 | test -f tmp/index.html 31 | test -f tmp/js/main.js 32 | test ! -e tmp/js/out 33 | 34 | cd tmp 35 | 36 | # Restore files not created by this script 37 | git add docs/master/ 38 | git checkout -- README.md docs/ 39 | git add . 40 | git commit -m "Built site from $SHA" 41 | git push 42 | rm -rf tmp 43 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | --- 2 | coverage: 3 |  status: 4 |  patch: 5 |  default: 6 |  enabled: false 7 | -------------------------------------------------------------------------------- /demo/reagentdemo/common.cljs: -------------------------------------------------------------------------------- 1 | (ns reagentdemo.common 2 | (:require [reagent.core :as r])) 3 | 4 | (defn demo-component [{:keys [comp src complete no-heading]}] 5 | (r/with-let [showing (r/atom true)] 6 | [:div 7 | (when comp 8 | [:div.demo-example.clearfix 9 | [:a.demo-example-hide {:on-click (fn [e] 10 | (.preventDefault e) 11 | (swap! showing not) 12 | nil)} 13 | (if @showing "hide" "show")] 14 | (when-not no-heading 15 | [:h3.demo-heading "Example "]) 16 | (when @showing 17 | (if-not complete 18 | [:div.simple-demo [comp]] 19 | [comp]))]) 20 | (if @showing 21 | (if src 22 | [:div.demo-source.clearfix 23 | (when-not no-heading 24 | [:h3.demo-heading "Source"]) 25 | src] 26 | [:div.clearfix]))])) 27 | -------------------------------------------------------------------------------- /demo/reagentdemo/core.cljs: -------------------------------------------------------------------------------- 1 | (ns reagentdemo.core 2 | (:require [reagent.core :as r] 3 | [sitetools.core :as tools :refer [link]] 4 | [reagentdemo.intro :as intro] 5 | [reagentdemo.news :as news])) 6 | 7 | (def github {:href "https://github.com/reagent-project/reagent"}) 8 | 9 | (defn github-badge [] 10 | [:a.github-badge 11 | github 12 | [:img {:style {:position "absolute" :top 0 :left 0 :border 0} 13 | :alt "Fork me on GitHub" 14 | :src "https://s3.amazonaws.com/github/ribbons/forkme_left_orange_ff7600.png"}]]) 15 | 16 | (def index-page "/index.html") 17 | (def title "Minimalistic React for ClojureScript") 18 | 19 | (tools/register-page index-page [#'intro/main] title) 20 | 21 | (defn error-boundary [& children] 22 | (let [error (r/atom nil)] 23 | (r/create-class 24 | {:constructor (fn [this props]) 25 | :component-did-catch (fn [this e info]) 26 | :get-derived-state-from-error (fn [e] 27 | (reset! error e) 28 | #js {}) 29 | :reagent-render (fn [& children] 30 | (if @error 31 | [:div 32 | "Something went wrong." 33 | [:input 34 | {:type "button" 35 | :on-click #(reset! error nil) 36 | :value "Try again"}]] 37 | (into [:<>] children)))}))) 38 | 39 | (defn demo [& [test-component]] 40 | [error-boundary 41 | [:div 42 | [:div.nav>ul.nav 43 | [:li.brand [link {:href index-page} "Reagent:"]] 44 | [:li [link {:href index-page} "Intro"]] 45 | [:li [link {:href news/url} "News"]] 46 | [:li>a github "GitHub"] 47 | [:li [:a {:href "http://reagent-project.github.io/docs/master/"} "API"]]] 48 | [:div test-component] 49 | [tools/main-content] 50 | [github-badge]]]) 51 | 52 | (defn init! [& [test-component]] 53 | (tools/start! {:body [#'demo test-component] 54 | :title-prefix "Reagent: " 55 | :css-infiles ["site/public/css/examples.css" 56 | "site/public/css/main.css"]})) 57 | -------------------------------------------------------------------------------- /demo/reagentdemo/dev.cljs: -------------------------------------------------------------------------------- 1 | (ns reagentdemo.dev 2 | "Initializes the demo app, and runs the tests." 3 | (:require [reagentdemo.core :as core] 4 | [reagenttest.runtests :as tests])) 5 | 6 | (enable-console-print!) 7 | 8 | (defn ^:dev/after-load init! [] 9 | (core/init! (tests/init!))) 10 | 11 | (init!) 12 | -------------------------------------------------------------------------------- /demo/reagentdemo/news.cljs: -------------------------------------------------------------------------------- 1 | (ns reagentdemo.news 2 | (:require [reagentdemo.news.anyargs :as anyargs] 3 | [reagentdemo.news.async :as async] 4 | [reagentdemo.news.undodemo :as undodemo] 5 | [reagentdemo.news.clockpost :as clock] 6 | [reagentdemo.news.news050 :as news050] 7 | [reagentdemo.news.news051 :as news051] 8 | [reagentdemo.news.news060 :as news060] 9 | [reagentdemo.news.news061 :as news061] 10 | [reagentdemo.news.news060rc :as news060rc] 11 | [reagentdemo.news.news060release :as news060r] 12 | [sitetools.core :as tools])) 13 | 14 | (defn main [] 15 | [:div 16 | [:div.reagent-demo 17 | [:h1 [:a {:href "https://github.com/reagent-project/reagent/blob/master/CHANGELOG.md"} "Check changelog for the latest releases"]]] 18 | 19 | [:div.reagent-demo 20 | [:h1 [:a {:href "https://github.com/reagent-project/reagent/blob/master/CHANGELOG.md"} "Reagent 1.0.0-alpha"]]] 21 | 22 | [:div.reagent-demo 23 | [:h1 [:a {:href "https://github.com/reagent-project/reagent/blob/master/CHANGELOG.md#0100-2020-03-06"} "Reagent 0.10.0"]] 24 | [:span "2020-03-06"]] 25 | 26 | [:div.reagent-demo 27 | [:h1 [:a {:href "https://github.com/reagent-project/reagent/blob/master/CHANGELOG.md#091-2020-01-15"} "Reagent 0.9.1"]] 28 | [:span "2020-01-15"]] 29 | 30 | [:div.reagent-demo 31 | [:h1 [:a {:href "https://github.com/reagent-project/reagent/blob/master/CHANGELOG.md#081-2018-05-15"} "Reagent 0.8.1"]] 32 | [:span "2018-05-15"]] 33 | 34 | [:div.reagent-demo 35 | [:h1 [:a {:href "https://github.com/reagent-project/reagent/blob/master/CHANGELOG.md#080-2018-04-19"} "Reagent 0.8.0"]] 36 | [:span "2018-04-19"]] 37 | 38 | [:div.reagent-demo 39 | [:h1 [:a {:href "https://github.com/reagent-project/reagent/blob/master/CHANGELOG.md#070-2017-06-27"} "Reagent 0.7.0"]] 40 | [:span "2017-06-27"]] 41 | 42 | [:div.reagent-demo 43 | [:h1 [:a {:href "https://github.com/reagent-project/reagent/blob/master/CHANGELOG.md#062-2017-05-19"} "Reagent 0.6.2"]] 44 | [:span "2017-05-19"]] 45 | 46 | [news061/main {:summary true}] 47 | [news060r/main {:summary true}] 48 | [news060rc/main {:summary true}] 49 | [news060/main {:summary true}] 50 | [news051/main {:summary true}] 51 | [news050/main {:summary true}] 52 | [clock/main {:summary true}] 53 | [anyargs/main {:summary true}] 54 | [async/main {:summary true}] 55 | [undodemo/main {:summary true}] 56 | [:div.reagent-demo 57 | [:h1 "Reagent 0.1.0"] 58 | [:span "2014-01-10"]] 59 | [:div.reagent-demo 60 | [:h1 "Reagent 0.0.2"] 61 | [:span "2013-12-17"]]]) 62 | 63 | (def url "/news/index.html") 64 | (tools/register-page url [#'main] "News") 65 | -------------------------------------------------------------------------------- /demo/reagentdemo/news/anyargs.cljs: -------------------------------------------------------------------------------- 1 | (ns reagentdemo.news.anyargs 2 | (:require [reagent.core :as r] 3 | [reagentdemo.syntax :as s] 4 | [sitetools.core :as tools :refer [link]] 5 | [reagentdemo.common :as common :refer [demo-component]] 6 | [geometry.core :as geometry])) 7 | 8 | (def url "/news/any-arguments.html") 9 | (def title "All arguments allowed") 10 | 11 | (def ns-src (s/syntaxed "(ns example 12 | (:require [reagent.core :as r]))")) 13 | 14 | (defn hello-component [name] 15 | [:p "Hello, " name "!"]) 16 | 17 | (defn say-hello [] 18 | [hello-component "world"]) 19 | 20 | (defn geometry-example [] 21 | [geometry/main {:width "100%" :height 500}]) 22 | 23 | (defn my-div [] 24 | (let [this (r/current-component)] 25 | (into [:div.custom (r/props this)] 26 | (r/children this)))) 27 | 28 | (defn call-my-div [] 29 | [:div 30 | [my-div "Some text."] 31 | [my-div {:style {:font-weight 'bold}} 32 | [:p "Some other text in bold."]]]) 33 | 34 | (defn main [{:keys [summary]}] 35 | (let [geometry {:href "https://github.com/reagent-project/reagent/tree/master/examples/geometry"} 36 | jonase {:href "https://github.com/jonase"}] 37 | [:div.reagent-demo 38 | [:h1 [link {:href url} title]] 39 | [:span "2014-02-15"] 40 | 41 | [:div.demo-text 42 | 43 | [:h2 "If it looks like a function…"] 44 | 45 | [:p "Calling a component in Reagent looks a lot like a function 46 | call. Now it also " [:em "works"] " like one."] 47 | 48 | [:p "Before 0.4.0, component functions were always called with 49 | three arguments: a map of attributes, a vector of ”children”, 50 | and the current React component."] 51 | 52 | [:p "This was confusing, and an unnecessary limitation, so now 53 | component functions get exactly the same arguments you pass to 54 | them."] 55 | 56 | (if summary 57 | [link {:href url :class 'news-read-more} "Read more"] 58 | [:div.demo-text 59 | [:p "In other words, you can now do this:"] 60 | 61 | [demo-component {:comp say-hello 62 | :src (s/src-of [:hello-component :say-hello])}] 63 | 64 | [:p "In the above example, it wouldn’t make any difference at 65 | all if " [:code "hello-component"] " had been called as a 66 | function, i.e with parentheses instead of brackets (except 67 | for performance, since components are cached between renders 68 | if the arguments to them don’t change)."] 69 | 70 | [:p "But there is one drawback: component function no longer 71 | receives the ”current component” as a parameter. Instead 72 | you’ll have to use " 73 | [:code "reagent.core/current-component"] 74 | " in order to get that. Beware that " 75 | [:code "current-component"] " is only valid in component 76 | functions, and must be called outside of e.g event handlers 77 | and " [:code "for"] " expressions, so it’s safest to always 78 | put the call at the top, as in " [:code "my-div"] " here:"] 79 | 80 | [demo-component {:comp call-my-div 81 | :src [:pre 82 | ns-src 83 | (s/src-of [:my-div :call-my-div])]}] 84 | 85 | [:p [:em "Note: "] [:code "r/props"] " and " 86 | [:code "r/children"] " correspond to React’s " 87 | [:code "this.props"] " and " [:code "this.props.children"] ", 88 | respectively. They may be convenient to use when wrapping 89 | native React components, since they follow the same 90 | conventions when interpreting the arguments given."] 91 | 92 | [:h2 "Other news in 0.4.0"] 93 | 94 | [:ul 95 | 96 | [:li "React has been updated to version 0.9.0."] 97 | 98 | [:li "You can now use any object that satisfies " 99 | [:code "ifn?"] " as a component function, and not just 100 | plain functions. That includes functions defined with " 101 | [:code "deftype"] ", " [:code "defrecord"] ", etc, as well 102 | as collections like maps."] 103 | 104 | [:li 105 | [:code "reagent.core/set-state"] " and " 106 | [:code "reagent.core/replace-state"] " are now implemented 107 | using an " [:code "reagent.core/atom"] ", and are 108 | consequently async."] 109 | 110 | [:li "Keys associated with items in a seq (e.g ”dynamic 111 | children” in React parlance) can now be specified with 112 | meta-data, as well as with a " [:code ":key"] " item in the 113 | first parameter as before. In other words, these two forms 114 | are now equivalent: " [:code "^{:key foo} [:li bar]"] " 115 | and " [:code "[:li {:key foo} bar]"] "."] 116 | 117 | [:li "Performance has been improved even further. For 118 | example, there is now practically no overhead for 119 | tracking derefs in components that don’t use " 120 | [:code "atom"] "s. Allocations and memory use have also 121 | been reduced."] 122 | 123 | [:li "Intro and examples have been tweaked a little to take 124 | advantage of the new calling conventions."]] 125 | 126 | [:h2 "New svg example"] 127 | 128 | [:p "There is also a new, elegant and simple " 129 | [:a geometry "example"] " of using svg with Reagent, written 130 | by " [:a jonase "Jonas Enlund"] ". It also shows how you can 131 | use Reagent’s new calling convensions, and looks like 132 | this:"] 133 | 134 | [demo-component {:comp geometry-example}]])]])) 135 | 136 | (tools/register-page url [#'main] title) 137 | -------------------------------------------------------------------------------- /demo/reagentdemo/news/binaryclock.cljs: -------------------------------------------------------------------------------- 1 | (ns reagentdemo.news.binaryclock 2 | (:require [reagent.core :as r])) 3 | 4 | (defn cell [n bit] 5 | [:div.clock-cell {:class (if (bit-test n bit) 6 | "light" 7 | "dark")}]) 8 | 9 | (defn column [n] 10 | [:div.clock-col 11 | [cell n 3] 12 | [cell n 2] 13 | [cell n 1] 14 | [cell n 0] 15 | [:div.clock-cell n]]) 16 | 17 | (defn column-pair [n] 18 | [:div.clock-pair 19 | [column (quot n 10)] 20 | [column (mod n 10)]]) 21 | 22 | (defn legend [& items] 23 | (into [:div.clock-col.clock-legend] 24 | (map (partial vector :div.clock-cell) 25 | items))) 26 | 27 | (defn clock [date show-100s toggle-100s] 28 | [:div.clock-main {:on-click toggle-100s 29 | :class (when show-100s "wide")} 30 | [legend 8 4 2 1] 31 | [column-pair (.getHours date)] 32 | [column-pair (.getMinutes date)] 33 | [column-pair (.getSeconds date)] 34 | (when show-100s 35 | [column-pair (-> (.getMilliseconds date) 36 | (quot 10))])]) 37 | 38 | (def clock-state (r/atom {:time (js/Date.) 39 | :show-100s false})) 40 | 41 | (defn update-time [] 42 | (swap! clock-state assoc :time (js/Date.))) 43 | 44 | (defn main [] 45 | (let [{:keys [time show-100s]} @clock-state] 46 | (if show-100s 47 | (r/next-tick update-time) 48 | (js/setTimeout update-time 1000)) 49 | [clock time show-100s 50 | #(swap! clock-state update-in [:show-100s] not)])) 51 | -------------------------------------------------------------------------------- /demo/reagentdemo/news/clockpost.cljs: -------------------------------------------------------------------------------- 1 | (ns reagentdemo.news.clockpost 2 | (:require [reagentdemo.syntax :as s] 3 | [sitetools.core :as tools :refer [link]] 4 | [reagentdemo.common :as common :refer [demo-component]] 5 | [reagentdemo.news.binaryclock :as binaryclock])) 6 | 7 | (def url "/news/binary-clock.html") 8 | (def title "A binary clock") 9 | 10 | (defn fn-src [src] 11 | [demo-component {:src src :no-heading true}]) 12 | 13 | (defn main [{:keys [summary]}] 14 | (let [lexclock {:href "http://www.lexicallyscoped.com/2014/01/23/clojurescript-react-om-binary-clock.html"} 15 | hopclock {:href "http://pmbauer.github.io/2014/01/27/hoplon-binary-clock/"} 16 | om {:href "https://github.com/swannodette/om"} 17 | hoplon {:href "http://hoplon.io"} 18 | clocksrc {:href "https://github.com/reagent-project/reagent/blob/master/demo/reagentdemo/news/binaryclock.cljs"}] 19 | 20 | [:div.reagent-demo 21 | [:h1 [link {:href url} title]] 22 | [:span "2014-02-26"] 23 | [:div.demo-text 24 | 25 | (when-not summary 26 | [:div 27 | [:div.clearfix 28 | [binaryclock/main]] 29 | [:div [:strong "Click to toggle 1/100th seconds."]]]) 30 | 31 | [:p "Fredrik Dyrkell wrote a very nice " [:a lexclock "binary 32 | clock"] " using " [:a om "Om"] ". I thought I’d replicate that 33 | using Reagent for fun (another re-write, using " 34 | [:a hoplon "Hoplon"] ", can be seen " [:a hopclock "here"] ")."] 35 | 36 | [:p "So, without further ado, here is a binary clock using Reagent."] 37 | 38 | (if summary 39 | [link {:href url :class 'news-read-mode} "Read more"] 40 | [:div.demo-text 41 | 42 | [fn-src (s/syntaxed "(ns example 43 | (:require [reagent.core :as r]))")] 44 | 45 | [:p "We start with the basics: The clock is built out of 46 | cells, with a light colour if the bit the cell corresponds to 47 | is set."] 48 | 49 | [fn-src (s/src-of [:cell] 50 | "reagentdemo/news/binaryclock.cljs")] 51 | 52 | [:p "Cells are combined into columns of four bits, with a 53 | decimal digit at the bottom."] 54 | 55 | [fn-src (s/src-of [:column] 56 | "reagentdemo/news/binaryclock.cljs")] 57 | 58 | [:p "Columns are in turn combined into pairs:"] 59 | 60 | [fn-src (s/src-of [:column-pair] 61 | "reagentdemo/news/binaryclock.cljs")] 62 | 63 | [:p "We'll also need the legend on the left side:"] 64 | 65 | [fn-src (s/src-of [:legend] 66 | "reagentdemo/news/binaryclock.cljs")] 67 | 68 | [:p "We combine these element into a component that shows the 69 | legend, hours, minutes and seconds; and optionally 1/100 70 | seconds. It also responds to clicks."] 71 | 72 | [fn-src (s/src-of [:clock] 73 | "reagentdemo/news/binaryclock.cljs")] 74 | 75 | [:p "We also need to keep track of the time, and of the 76 | detail shown, in a Reagent atom. And a function to update the 77 | time."] 78 | 79 | [fn-src (s/src-of [:clock-state :update-time] 80 | "reagentdemo/news/binaryclock.cljs")] 81 | 82 | [:p "And finally we use the " [:code "clock"] " component. 83 | The current time is scheduled to be updated, after a suitable 84 | delay, every time the main component is rendered (" 85 | [:code "reagent.core/next-tick"] " is just a front for " 86 | [:code "requestAnimationFrame"] "):"] 87 | 88 | [fn-src (s/src-of [:main] 89 | "reagentdemo/news/binaryclock.cljs")] 90 | 91 | [:p "The entire source is also available " 92 | [:a clocksrc "here"] "."] 93 | 94 | [:h2 "How it all works"] 95 | 96 | [:p "Reading through the source, it may look like the entire 97 | clock component is recreated from scratch whenever the time 98 | changes. "] 99 | 100 | [:p "That is an illusion: Reagent and React together 101 | makes sure that only the parts of the DOM that actually need 102 | to change are updated. For example, the " 103 | [:code "column-pair"] " function corresponding to hours only 104 | runs once every hour."] 105 | 106 | [:p "And that’s what makes Reagent and React fast. Try 107 | clicking on the clock to toggle the display of 1/100th 108 | seconds. Most browsers should have no trouble at all keeping 109 | up (even if they won’t actually show every 1/100th second: 110 | they are typically limited to roughly 60 fps)."] 111 | 112 | [:p "But it is a very handy illusion. Almost the entire UI is 113 | made up of pure functions, transforming immutable data into 114 | other immutable data structures. That makes them easy to 115 | reason about, and trivial to test. You don’t have to care 116 | about ”model objects”, or about how to update the DOM 117 | efficiently. "] 118 | 119 | [:p "Just pass arguments to component functions, return a UI 120 | description that corresponds to those arguments, and leave it 121 | to React to actually display that UI."]])]])) 122 | 123 | (tools/register-page url [#'main] title) 124 | -------------------------------------------------------------------------------- /demo/reagentdemo/news/news051.cljs: -------------------------------------------------------------------------------- 1 | (ns reagentdemo.news.news051 2 | (:require [reagent.core :as r] 3 | [reagentdemo.syntax :as s] 4 | [sitetools.core :as tools :refer [link]] 5 | [reagentdemo.common :as common :refer [demo-component]] 6 | [clojure.string :as string])) 7 | 8 | (def url "/news/news051.html") 9 | (def title "News in 0.5.1") 10 | 11 | (def ns-src (s/syntaxed "(ns example.core 12 | (:require [reagent.core :as r]))")) 13 | 14 | (defn old-and-tired [] 15 | [:ul 16 | [:li.foo [:a.bar "Text 1"]] 17 | [:li.foo [:a.bar "Text 2"]]]) 18 | 19 | (defn new-hotness [] 20 | [:ul 21 | [:li.foo>a.bar "Text 1"] 22 | [:li.foo>a.bar "Text 2"]]) 23 | 24 | (def upper-value (r/atom "FOOBAR")) 25 | 26 | (defn upper-input [] 27 | [:div 28 | [:p "Value is: " @upper-value] 29 | [:input {:type 'text :value @upper-value 30 | :on-change #(reset! upper-value 31 | (-> % .-target .-value string/upper-case))}]]) 32 | 33 | 34 | (defn main [{:keys [summary]}] 35 | [:div.reagent-demo 36 | [:h1 [link {:href url} title]] 37 | [:span "2015-09-09"] 38 | [:div.demo-text 39 | [:p "Reagent 0.5.1 contains a new convenient shortcut for nested 40 | elements, better error messages, new logic for maintaining cursor 41 | position in inputs, a new version of React, and some bug fixes and 42 | improvements."] 43 | 44 | (if summary 45 | [link {:href url :class 'news-read-more} "Read more"] 46 | [:div.demo-text 47 | [:h2 "New syntax for nested elements"] 48 | 49 | [:p "The ”Hiccup syntax” used in Reagent now supports defining 50 | nested elements using ”>” as a separator in keywords. This is 51 | probably easier to show than to explain…"] 52 | 53 | [:p "So, instead of doing this: "] 54 | 55 | [demo-component {:comp old-and-tired 56 | :src (s/src-of [:old-and-tired])}] 57 | 58 | [:p "you can now do this: "] 59 | 60 | [demo-component {:comp new-hotness 61 | :src (s/src-of [:new-hotness])}] 62 | 63 | [:p "with identical results, thus saving several square 64 | brackets from an untimely death."] 65 | 66 | 67 | [:h2 "Keeping position"] 68 | 69 | [:p "Reagent now tries harder to maintain cursor position in 70 | text inputs, even when the value of the input is transformed in 71 | code."] 72 | 73 | [:p "Previously, the cursor would jump to the end of the text 74 | whenever you made a change in the middle of the text in 75 | something like this:"] 76 | 77 | [demo-component {:comp upper-input 78 | :src [:pre ns-src 79 | (s/src-of [:upper-value :upper-input])]}] 80 | 81 | 82 | [:h2 "Other news"] 83 | 84 | [:ul 85 | [:li "React is updated to 0.13.3."] 86 | 87 | [:li "A bit better error messages. In particular, the current 88 | component path is now printed when an exception is thrown."] 89 | 90 | [:li "There is a new 91 | function, " [:code "reagent.core/force-update"] " that will 92 | make a component render immediately. It takes an optional 93 | second parameter that forces re-rendering of every child 94 | component as well."] 95 | 96 | [:li "Calling the result 97 | of " [:code "reagent.core/create-class"] " as a function is 98 | now deprecated. Use Hiccup syntax instead."] 99 | 100 | [:li "Some other bug fixes and performance tweaks."]]])]]) 101 | 102 | (tools/register-page url [#'main] title) 103 | -------------------------------------------------------------------------------- /demo/reagentdemo/news/news060rc.cljs: -------------------------------------------------------------------------------- 1 | (ns reagentdemo.news.news060rc 2 | (:require [reagent.core :as r] 3 | [reagentdemo.syntax :as s] 4 | [sitetools.core :as tools :refer [link]] 5 | [reagentdemo.news.news060 :as news060] 6 | [reagentdemo.common :as common :refer [demo-component]])) 7 | 8 | (def url "/news/news060-rc.html") 9 | (def title "Reagent 0.6.0-rc") 10 | 11 | (def ns-src (s/syntaxed "(ns example.core 12 | (:require [reagent.core :as r]))")) 13 | 14 | (defn mixed [] 15 | [:div 16 | "Symbols are " 'ok " as well as " :keywords "."]) 17 | 18 | (def some-atom (r/atom 0)) 19 | 20 | (defn confusion-avoided [] 21 | [:div "This is some atom: " some-atom]) 22 | 23 | 24 | (defn main [{:keys [summary]}] 25 | [:div.reagent-demo 26 | [:h1 [link {:href url} title]] 27 | [:span "2016-09-14"] 28 | [:div.demo-text 29 | [:p "Reagent 0.6.0-rc has been given a lot of testing, a new 30 | version of React (15.1.0), bug fixing and some small general 31 | improvements since 0.6.0-alpha. It has one new feature: general 32 | ClojureScript objects can now be used anywhere in markup 33 | content."] 34 | 35 | (if summary 36 | [link {:href url :class 'news-read-more} "Read more"] 37 | [:div.demo-text 38 | [:section.demo-text 39 | [:p "See " [link {:href news060/url} "this 40 | article"] " for more information about Reagent 0.6.0."] 41 | 42 | [:h2 "Generalized markup"] 43 | 44 | [:p "Symbols and keywords can now be used in markup content 45 | like this: "] 46 | 47 | [demo-component {:comp mixed 48 | :src (s/src-of [:mixed])}] 49 | 50 | [:p "This makes content conversions behave the same as in 51 | attributes, where symbols and keywords have been supported 52 | before. "] 53 | 54 | [:p "But mainly it avoids confusing error messages when you 55 | happen to drop an arbitrary ClojureScript object into the 56 | markup, like this: "] 57 | 58 | [demo-component {:comp confusion-avoided 59 | :src (s/src-of [:some-atom 60 | :confusion-avoided])}] 61 | 62 | [:p "This may not be particularly useful, but it is at least a 63 | lot better than getting a quite confusing error message from 64 | React, that no longer accepts unknown objects…"] 65 | 66 | [:p "Any object hat satisfies IPrintWithWriter is allowed, and 67 | is converted to a string using " [:code "pr-str" "."]]]])]]) 68 | 69 | 70 | (tools/register-page url [#'main] title) 71 | -------------------------------------------------------------------------------- /demo/reagentdemo/news/news060release.cljs: -------------------------------------------------------------------------------- 1 | (ns reagentdemo.news.news060release 2 | (:require [reagentdemo.syntax :as s] 3 | [sitetools.core :as tools :refer [link]] 4 | [reagentdemo.news.news060 :as news060] 5 | [reagentdemo.news.news060rc :as news060rc])) 6 | 7 | (def url "/news/news060.html") 8 | (def title "Reagent 0.6.0") 9 | 10 | (def ns-src (s/syntaxed "(ns example.core 11 | (:require [reagent.core :as r]))")) 12 | 13 | (def changelog 14 | "https://github.com/reagent-project/reagent/blob/master/CHANGELOG.md") 15 | 16 | (defn abstract [] 17 | [:div.demo-text 18 | [:p 19 | "Reagent 0.6.0 has a new version of React (15.2.1), and a few 20 | bug fixes. Otherwise it is identical to 0.6.0-rc."]]) 21 | 22 | (defn story [] 23 | [:div.demo-text 24 | [:p 25 | "See " [link {:href news060/url} "this story"] 26 | " for much more information about Reagent 0.6.0."] 27 | [:p 28 | "You can also have a look at the " 29 | [link {:href news060rc/url} "news in 0.6.0-rc"] 30 | " and the " [link {:href changelog} "change log"] 31 | "."]]) 32 | 33 | (defn main [{:keys [summary]}] 34 | [:div.reagent-demo 35 | [:h1 36 | [link {:href url} title]] 37 | [:span "2016-06-09"] 38 | [:div 39 | [abstract] 40 | (if summary 41 | [link {:href url :class 'news-read-more} "Read more"] 42 | [:section.demo-text 43 | [story]])]]) 44 | 45 | (tools/register-page url [#'main] title) 46 | -------------------------------------------------------------------------------- /demo/reagentdemo/news/news061.cljs: -------------------------------------------------------------------------------- 1 | (ns reagentdemo.news.news061 2 | (:require [reagentdemo.syntax :as s] 3 | [sitetools.core :as tools :refer [link]])) 4 | 5 | (def url "/news/news061.html") 6 | (def title "Reagent 0.6.1") 7 | 8 | (def ns-src (s/syntaxed "(ns example.core 9 | (:require [reagent.core :as r]))")) 10 | 11 | (def changelog 12 | "https://github.com/reagent-project/reagent/blob/master/CHANGELOG.md") 13 | 14 | (defn abstract [] 15 | [:div.demo-text 16 | [:p 17 | "Reagent 0.6.1 has a new version of React (15.4.0), and it fixes a bug with 18 | " [:code ":ref"] " attributes on " [:code "input"] " elements: " [:a {:href "https://github.com/reagent-project/reagent/issues/259"} "#259"] "."]]) 19 | 20 | (defn main [{:keys [summary]}] 21 | [:div.reagent-demo 22 | [:h1 23 | [link {:href url} title]] 24 | [:span "2017-03-11"] 25 | [:div 26 | [abstract]]]) 27 | 28 | (tools/register-page url [#'main] title) 29 | -------------------------------------------------------------------------------- /demo/reagentdemo/news/undodemo.cljs: -------------------------------------------------------------------------------- 1 | (ns reagentdemo.news.undodemo 2 | (:require [reagent.core :as r] 3 | [reagentdemo.syntax :as s] 4 | [sitetools.core :as tools :refer [link]] 5 | [reagentdemo.common :as common :refer [demo-component]] 6 | [todomvc.core :as todomvc])) 7 | 8 | (def url "/news/cloact-reagent-undo-demo.html") 9 | (def title "Cloact becomes Reagent: Undo is trivial") 10 | 11 | (def ns-src (s/syntaxed "(ns example 12 | (:require [reagent.core :as r]))")) 13 | 14 | (def state todomvc/todos) 15 | 16 | (def undo-list (r/atom nil)) 17 | 18 | (defn undo [] 19 | (let [undos @undo-list] 20 | (when-let [old (first undos)] 21 | (reset! state old) 22 | (reset! undo-list (rest undos))))) 23 | 24 | (defn undo-button [] 25 | (let [n (count @undo-list)] 26 | [:input {:type "button" :on-click undo 27 | :disabled (zero? n) 28 | :value (str "Undo (" n ")")}])) 29 | 30 | (defn todomvc-with-undo [] 31 | (add-watch state ::undo-watcher 32 | (fn [_ _ old-state _] 33 | (swap! undo-list conj old-state))) 34 | [:div 35 | [undo-button] 36 | [todomvc/todo-app]]) 37 | 38 | (defn undo-demo [] 39 | [demo-component {:comp todomvc-with-undo 40 | :src [:pre ns-src 41 | (s/src-of [:state :undo-list :undo :save-state 42 | :undo-button :todomvc-with-undo])]}]) 43 | 44 | (def undo-demo-cleanup 45 | (with-meta undo-demo {:component-will-unmount 46 | (fn [] 47 | (reset! undo-list nil) 48 | (remove-watch state ::undo-watcher))})) 49 | 50 | (defn main [{:keys [summary]}] 51 | (let [head title] 52 | [:div.reagent-demo 53 | [:h1 [link {:href url} head]] 54 | ;; Undo demo originall added on 0a0dbdee7dfdaa545171d41fa1c3a18a1cae6e1b 55 | [:span "2014-01-19"] 56 | 57 | [:div.demo-text 58 | [:h2 "(reset! cloact-name \"Reagent\")"] 59 | 60 | [:p "It turns out that ”Cloact” was a really, really bad 61 | name. It made some people think about birds’ behinds, in 62 | possibly unhealthy ways, which even Google suggested they 63 | should."] 64 | 65 | [:p "The new name is " [:strong "Reagent"] ", which hopefully 66 | doesn’t bring with it the same disgusting connotations."] 67 | 68 | [:p "The API is otherwise unchanged, so a simple 69 | search-and-replace should suffice."] 70 | 71 | (if summary 72 | [link {:href url 73 | :class 'news-read-more} "Read more"] 74 | [:div.demo-text 75 | 76 | [:h2 "Undo the easy way"] 77 | 78 | [:p "To celebrate the undoing of the apparently disgusting 79 | name, here is an example of how easy it is to add undo 80 | functionality to Reagent components."] 81 | 82 | [:p "It simply saves the old state whenever it changes, and 83 | restores it when the button is clicked."] 84 | 85 | [:p "The really nice thing about ClojureScript is that not 86 | only is this easy and safe to do, courtesy of immutable data 87 | structures, it is also efficient. ClojureScript figures out 88 | how to represent ”changes” to maps and vectors efficiently, 89 | so that you won’t have to."] 90 | 91 | [undo-demo-cleanup]])]])) 92 | 93 | (tools/register-page url [#'main] title) 94 | -------------------------------------------------------------------------------- /demo/reagentdemo/prod.cljs: -------------------------------------------------------------------------------- 1 | (ns reagentdemo.prod 2 | "Initializes the demo app" 3 | (:require [reagentdemo.core :as core])) 4 | 5 | (core/init!) 6 | 7 | -------------------------------------------------------------------------------- /demo/reagentdemo/syntax.clj: -------------------------------------------------------------------------------- 1 | (ns reagentdemo.syntax 2 | (:require [clojure.java.io :as io] 3 | [clojure.string :as string] 4 | [cljs.analyzer :as analyzer])) 5 | 6 | ;;; Source splitting 7 | 8 | (defn src-parts [src] 9 | (string/split src #"\n(?=[(])")) 10 | 11 | (defn src-defs [parts] 12 | (let [ws #"[^ \t\n]+"] 13 | (into {} (for [x parts] 14 | [(->> x (re-seq ws) second keyword) x])))) 15 | 16 | (defn fun-map [src] 17 | (-> src src-parts src-defs)) 18 | 19 | (defn src-for-names [srcmap names] 20 | (string/join "\n" (map srcmap names))) 21 | 22 | 23 | ;;; Macros 24 | 25 | (defmacro syntaxed [src] 26 | `(reagentdemo.syntax/syntaxify ~src)) 27 | 28 | ;; ;; A much simpler way to find source: currently broken with #js annotations 29 | ;; (defmacro src-for [& syms] 30 | ;; (let [s (map #(list 'with-out-str (list 'cljs.repl/source %)) syms)] 31 | ;; `(->> [~@s] 32 | ;; (string/join "\n") 33 | ;; syntaxed))) 34 | 35 | ;; (defmacro src-from-file [f] 36 | ;; (let [src (-> f io/resource slurp)] 37 | ;; `(syntaxed ~src))) 38 | 39 | (defmacro src-of 40 | ([funs] 41 | `(src-of ~funs nil)) 42 | ([funs resource] 43 | (assert (or (nil? funs) 44 | (vector? funs))) 45 | (assert (or (nil? resource) 46 | (string? resource))) 47 | (let [f (if (nil? resource) 48 | (-> (name analyzer/*cljs-ns*) 49 | (string/replace #"[.]" "/") 50 | (str ".cljs")) 51 | resource) 52 | src (-> f io/resource slurp) 53 | sel (if (nil? funs) 54 | src 55 | (-> src fun-map (src-for-names funs)))] 56 | `(syntaxed ~sel)))) 57 | -------------------------------------------------------------------------------- /demo/reagentdemo/syntax.cljs: -------------------------------------------------------------------------------- 1 | (ns reagentdemo.syntax 2 | (:require-macros reagentdemo.syntax) 3 | (:require [clojure.string :as string])) 4 | 5 | ;; Styles for syntax highlighting 6 | 7 | (def comment-style {:style {:color "gray" :font-style "italic"}}) 8 | (def string-style {:style {:color "green"}}) 9 | (def keyword-style {:style {:color "blue"}}) 10 | (def builtin-style {:style {:color "#687868" :font-weight "bold"}}) 11 | (def def-style {:style {:color "#5050c0" :font-weight "bold"}}) 12 | 13 | (def paren-style-1 {:style {:color "#272"}}) 14 | (def paren-style-2 {:style {:color "#940"}}) 15 | (def paren-style-3 {:style {:color "#44a"}}) 16 | 17 | 18 | ;;;;; Colorization 19 | 20 | (def builtins #{"def" "defn" "defonce" "ns" "atom" "let" "if" "when" 21 | "cond" "merge" "assoc" "swap!" "reset!" "for" 22 | "range" "nil?" "int" "or" "->" "->>" "%" "fn" "if-not" 23 | "empty?" "case" "str" "pos?" "zero?" "map" "remove" 24 | "empty" "into" "assoc-in" "dissoc" "get-in" "when-not" 25 | "filter" "vals" "count" "complement" "identity" "dotimes" 26 | "update-in" "sorted-map" "inc" "dec" "false" "true" "not" 27 | "=" "partial" "first" "second" "rest" "list" "conj" 28 | "drop" "when-let" "if-let" "add-watch" "mod" "quot" 29 | "bit-test" "vector" "do" "try" "catch" "finally"}) 30 | 31 | (def styles {:comment comment-style 32 | :str-litt string-style 33 | :keyw keyword-style 34 | :builtin builtin-style 35 | :def def-style}) 36 | 37 | (def paren-styles [paren-style-1 paren-style-2 paren-style-3]) 38 | 39 | (def tokenize-pattern 40 | (let [ws " \\t\\n" 41 | open "\\[({" 42 | close ")\\]}" 43 | sep (str ws open close) 44 | comment-p ";.*" 45 | str-p "\"[^\"]*\"" 46 | open-p (str "[" open "]") 47 | close-p (str "[" close "]") 48 | iden-p (str "[^" sep "]+") 49 | meta-p (str "\\^" iden-p) 50 | any-p (str "[" ws "]+|\\^[^" sep "]+|.")] 51 | (re-pattern (str "(" 52 | (string/join ")|(" [comment-p str-p open-p 53 | close-p meta-p iden-p any-p]) 54 | ")")))) 55 | 56 | (def keyw-re #"^:") 57 | (def qualif-re #"^[a-z]+/") 58 | (def def-re #"^def|^ns\b") 59 | 60 | (defn tokenize [src] 61 | (for [[s comment strlitt open close met iden any] 62 | (re-seq tokenize-pattern src)] 63 | (cond 64 | (some? comment) [:comment s] 65 | (some? strlitt) [:str-litt s] 66 | (some? open) [:open s] 67 | (some? close) [:close s] 68 | (some? met) [:other s] 69 | (some? iden) (cond (re-find keyw-re s) [:keyw s] 70 | (builtins s) [:builtin s] 71 | (re-find qualif-re s) [:builtin s] 72 | :else [:iden s]) 73 | (some? any) [:other s]))) 74 | 75 | (defn syntaxify [src] 76 | (let [ncol (count paren-styles) 77 | paren-style #(nth paren-styles (mod % ncol))] 78 | (loop [tokens (tokenize (str src " ")) 79 | prev nil 80 | level 0 81 | res []] 82 | (let [[kind val] (first tokens) 83 | level' (case kind 84 | :open (inc level) 85 | :close (dec level) 86 | level) 87 | style (case kind 88 | :iden (when (and prev (re-find def-re prev)) 89 | (:def styles)) 90 | :open (paren-style level) 91 | :close (paren-style level') 92 | (styles kind)) 93 | remain (rest tokens)] 94 | (if-not (empty? remain) 95 | (recur remain 96 | (case kind :other prev val) 97 | level' 98 | (if (nil? style) 99 | (let [old (peek res)] 100 | (if (string? old) 101 | (conj (pop res) (str old val)) 102 | (conj res val))) 103 | (conj res [:span style val]))) 104 | (into [:pre] res)))))) 105 | -------------------------------------------------------------------------------- /demo/sitetools/core.cljs: -------------------------------------------------------------------------------- 1 | (ns sitetools.core 2 | (:require [clojure.string :as string] 3 | [goog.events :as evt] 4 | ["react" :as react] 5 | [reagent.core :as r] 6 | [reagent.dom :as rdom] 7 | [reagent.dom.client :as rdomc]) 8 | (:import goog.History 9 | [goog.history Html5History EventType])) 10 | 11 | ;;; Configuration 12 | 13 | (declare main-content) 14 | 15 | (defonce root 16 | ;; Init only on use, this ns is loaded for SSR build also 17 | (delay (rdomc/create-root (js/document.getElementById "main-content")))) 18 | 19 | (defonce config (r/atom {:body [#'main-content] 20 | :pages {"/index.html" {:content [:div] 21 | :title ""}} 22 | :site-dir "target/prerender/public/" 23 | :css-infiles ["site/public/css/main.css"] 24 | :css-file "css/built.css" 25 | :js-file "js/main.js" 26 | :react-root root 27 | :default-title ""})) 28 | 29 | (defonce history nil) 30 | 31 | (defn demo-handler [state [id x :as event]] 32 | (case id 33 | :set-content (let [page x 34 | title (:title page) 35 | title (if title 36 | (str (:title-prefix state) title) 37 | (str (:default-title state)))] 38 | (when r/is-client 39 | (set! js/document.title title)) 40 | (assoc state :current-page page :title title)) 41 | :set-page (let [path x 42 | _ (assert (string? path)) 43 | ps (:pages state) 44 | p (get ps path (get ps "/index.html"))] 45 | (recur state [:set-content p])) 46 | :goto-page (let [path x 47 | _ (assert (string? path))] 48 | (when-some [h history] 49 | (r/after-render (fn [] 50 | (.setToken h x) 51 | (js/scrollTo 0 0))) 52 | state) 53 | (recur state [:set-page x])))) 54 | 55 | (defn emit [event] 56 | ;; (dbg event) 57 | (r/rswap! config demo-handler event)) 58 | 59 | (defn register-page [url comp title] 60 | {:pre [(re-matches #"/.*[.]html" url) 61 | (vector? comp)]} 62 | (swap! config update-in [:pages] 63 | assoc url {:content comp :title title})) 64 | 65 | 66 | ;;; History 67 | 68 | (defn init-history [page] 69 | (when-not history 70 | (let [html5 (and page 71 | (Html5History.isSupported) 72 | (#{"http:" "https:"} js/location.protocol)) 73 | h (if html5 74 | (doto (Html5History.) 75 | (.setUseFragment false) 76 | (.setPathPrefix (-> js/location.pathname 77 | (string/replace 78 | (re-pattern (str page "$")) "") 79 | (string/replace #"/*$" "")))) 80 | (History.))] 81 | (set! history h) 82 | (evt/listen history EventType.NAVIGATE 83 | (fn [^js/Event e] 84 | (when (.-isNavigation e) 85 | (emit [:set-page (.-token e)]) 86 | (r/flush)))) 87 | (.setEnabled history true) 88 | (let [token (.getToken history) 89 | p (if (and page (not html5) (empty? token)) 90 | page 91 | token)] 92 | (emit [:set-page p]))))) 93 | 94 | (defn to-relative [f] 95 | (string/replace f #"^/" "")) 96 | 97 | 98 | ;;; Components 99 | 100 | (defn link [props child] 101 | [:a (assoc props 102 | :href (-> props :href to-relative) 103 | :on-click #(do (.preventDefault %) 104 | (emit [:goto-page (:href props)]))) 105 | child]) 106 | 107 | (defn main-content [] 108 | (get-in @config [:current-page :content])) 109 | 110 | ;;; Main entry points 111 | 112 | (defn start! [site-config] 113 | (swap! config merge site-config) 114 | (when r/is-client 115 | (let [page-conf (when (exists? js/pageConfig) 116 | (js->clj js/pageConfig :keywordize-keys true)) 117 | conf (swap! config merge page-conf) 118 | {:keys [page-path body react-root]} conf] 119 | (init-history page-path) 120 | ;; Enable StrictMode to warn about e.g. findDOMNode 121 | (rdomc/render @react-root [:> react/StrictMode {} body])))) 122 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "test" "examples/todomvc/src" "examples/simple/src" "examples/geometry/src" "demo"] 2 | :deps {org.clojure/clojurescript {:mvn/version "1.11.132"} 3 | doo/doo {:mvn/version "0.1.11"} 4 | funcool/promesa {:mvn/version "11.0.678"}}} 5 | -------------------------------------------------------------------------------- /doc/0.8-upgrade.md: -------------------------------------------------------------------------------- 1 | # 0.8 Upgrade guide 2 | 3 | The necessary changes depend on what environment you target, and 4 | how you want to provide React. 5 | 6 | | | Build | Browser | Node | 7 | |---|---|---|---| 8 | | Cljsjs | `:none` | Supported | Requires Cljs 1.10.238+ | 9 | | Cljsjs | `:advanced` | Supported | Requires Cljs 1.10.238+ | 10 | | `node modules` | `:none` | Requires Cljs 1.10.312 | Supported | 11 | | `node modules` | `:advanced` | Requires Cljs 1.10.312 | Supported | 12 | 13 | ## Browser - Cljsjs 14 | 15 | **This is the recommended setup.** 16 | 17 | Using Reagent with Cljsjs packages doesn't require changes, 18 | other than making sure you update Cljsjs React dependencies, 19 | if you have direct dependencies to them. 20 | 21 | To ensure Cljsjs libs are used instead of Node Modules, you can add `:npm-deps false`, which will make sure ClojureScript compiler doesn't look into `node_modules` directory if it exists. 22 | 23 | ## Browser - node modules 24 | 25 | If `react`, `react-dom` and `create-react-class` are available in `node_modules` 26 | directory, ClojureScript compiler will use these with Reagent. 27 | 28 | If you don't want to call `npm` and manage `package.json`, you can use `:npm-deps` and `:install-deps` compiler options to 29 | have ClojureScript install the packages automatically. 30 | 31 | You can use `:process-shim` compiler option to provide `process.env.NODE_ENV` 32 | constant which is used by JS code to enable development and production 33 | builds. ClojureScript compiler will automatically set this constant to 34 | `production` value when using `:advanced` optimizations. This enables 35 | the React production build. 36 | 37 | When using module processing, it should be possible to split output into several 38 | [modules](https://clojurescript.org/reference/compiler-options#modules). 39 | 40 | **Externs are required for use with node modules also!** React created objects 41 | statically in several places and then accesses those dynamically. Closure-compiler 42 | will in these cases rename the statically object properties, which will break 43 | dynamically accessing the objects. Externs fix this by defining which properties 44 | must not be renamed. 45 | 46 | ## Browser - Webpack 47 | 48 | https://clojurescript.org/guides/webpack 49 | 50 | If you want to load React.js yourself from external JS file (CDN) or from custom bundle, 51 | it should be possible to override the Cljsjs foreign-libs, while still using externs from Cljsjs packages. To override the foreign-libs, you can provide following compiler option: 52 | 53 | ```clj 54 | :foreign-libs 55 | [{:file "bundje.js", 56 | :provides ["react" "react-dom" "create-react-class" "react-dom/server"], 57 | :global-exports {react React 58 | react-dom ReactDOM 59 | create-react-class createReactClass 60 | react-dom/server ReactDOMServer}}] 61 | ``` 62 | 63 | ## NodeJS - Cljsjs 64 | 65 | Requires https://github.com/clojure/clojurescript/commit/f7d611d87f6ea8a605eae7c0339f30b79a840b49 66 | 67 | Available in 1.10.238 68 | 69 | Reagent should use Cljsjs libraries by default even when running on Node. 70 | 71 | ## NodeJS - node modules 72 | 73 | Install `react`, `react-dom` and `create-react-class` npm packages, 74 | and ClojureScript should automatically use `require` to 75 | load React for Reagent. 76 | 77 | ## Electron 78 | 79 | ??? 80 | 81 | ## React-native 82 | 83 | https://github.com/drapanjanas/re-natal/issues/128 84 | 85 | ## Common Problems 86 | 87 | ### Mismatch with Cljsjs and npm packages 88 | 89 | If you have one npm package installed, e.g. `react`, you also need 90 | to provide others (`react-dom` and `create-react-class`), else 91 | Cljsjs packages would be used for these, and packages from different sources 92 | don't work together. 93 | 94 | ### Previous problems 95 | 96 | Before ClojureScript 1.10.312 there were couple of problems with npm support: 97 | 98 | 1. Closure can't properly handle React 16 CommonJS module pattern: https://github.com/google/closure-compiler/issues/2841 99 | This causes the production React code being loaded even for development builds. 100 | Using Chrome React Developer Tools with this setup will break Reagent. Fixed by `[com.google.javascript/closure-compiler-unshaded "v20180610"]` ([PR](https://github.com/google/closure-compiler/pull/2963)), will be 101 | the included in next the ClojureScript release. 102 | 103 | 2. Closure optimization currently breaks certain statically created objects which are 104 | accessed dynamically in `ReactDOM/server`: https://github.com/facebook/react/issues/12368 105 | Fixed by using `[com.google.javascript/closure-compiler-unshaded "v20180319"]` ([fix commit](https://github.com/google/closure-compiler/commit/c13cf48b98477e44409dba6359246bffa95b1c7b)), will be 106 | the default in next ClojureScript release. 107 | -------------------------------------------------------------------------------- /doc/ControlledInputs.md: -------------------------------------------------------------------------------- 1 | # Controlled inputs 2 | 3 | Reagent uses async rendering which causes problems with controlled inputs. If 4 | the input element is created directly by Reagent (i.e. `[:input ...]` in hiccup), [a 5 | workaround](https://github.com/reagent-project/reagent/blob/master/src/reagent/impl/input.cljs) 6 | can be applied, but if the input is created by JS library (i.e. JSX `` 7 | or React `create-element`), Reagent doesn't see 8 | the element so the workaround can't be applied. 9 | 10 | Due to async rendering, the DOM update doesn't occur during the event handler, 11 | but some time later. In certain cases, like when the cursor is not at the end 12 | of the input, updating the DOM input value causes the cursor to move to the 13 | end of the input. Without async rendering, browsers probably implement logic 14 | to keep the cursor position if the value is updated during event handler. 15 | 16 | Reagent workaround works by changing the React input element into 17 | uncontrolled input (i.e. the DOM value is not updated by React). Instead 18 | Reagent will update DOM itself if the Reagent input value property changes. 19 | This enables Reagent to check the cursor position before updating the 20 | value, and if needed, save and restore the cursor position 21 | after updating the value. 22 | 23 | For JS libraries, usually the best solution is if the library provides an option to 24 | use custom component to create the input element, which enables 25 | Reagent to create the input element: 26 | 27 | ## React-native 28 | 29 | ReactNative has it's own `TextInput` component. Similar workaround can't be (at least easily) implemented in ReactNative, as the component doesn't provide similar API as DOM Inputs to control the selection. Currently best option is to use uncontrolled inputs (`default-value` and `on-change`). If you also need to update the input value from your code, you could change to Input component React key to force recreation of the component: 30 | 31 | ```clj 32 | [:> TextInput 33 | {:key @k 34 | :default-value @v 35 | :on-change ...}] 36 | 37 | (reset! v "foo") 38 | (swap! k inc) 39 | ;; When key changes, old component is unmounted and new one created, and the new component will use the new default-value 40 | ``` 41 | 42 | (Similar workaround can be also used with DOM inputs) 43 | 44 | ## Examples 45 | 46 | - [Material UI](./examples/material-ui.md) 47 | - [Smooth UI](./examples/smooth-ui.md) 48 | -------------------------------------------------------------------------------- /doc/FAQ/CljsjsReactProblems.md: -------------------------------------------------------------------------------- 1 | # Question 2 | 3 | Reagent doesn't work after updating dependencies. 4 | 5 | # Answer 6 | 7 | If you see errors about accessing `React` or `ReactDOM` object or some React method after you have updated your dependencies, the problem is probably conflicting `cljsjs/react` or `cljsjs/react-dom` versions. Other dependencies than Reagent might bring in versions which don't work with Reagent, or a library which only depends on one of the packages might cause React and ReactDOM versions to conflict. 8 | 9 | To fix this you should check `lein deps :tree` or `boot show -d`, and check which version of Cljsjs React packages you have. 10 | 11 | There are three alternative solutions: 12 | 13 | 1. Update all the packages that require Cljsjs React packages to use same (or compatible) versions as Reagent 14 | 2. Add `:exclusion [cljsjs/react cljsjs/react-dom]` to problematic dependencies, so only Reagent 15 | will have transitive dependency on React packages 16 | 3. Add direct `cljsjs/react` and `cljsjs/react-dom` dependencies to your project, which will override any transitive dependencies 17 | 18 | Note: `cljsjs/react-dom-server` package is deprecated but Reagent still depends on empty package for compatibility. 19 | 20 | Note: For more information on how Leiningen and Boot resolve dependencies using Maven-resolver, read: https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html 21 | -------------------------------------------------------------------------------- /doc/FAQ/ComponentNotRerendering.md: -------------------------------------------------------------------------------- 1 | ### Question 2 | 3 | My component is not re-rendering, what's wrong? 4 | 5 | ### Answer 6 | 7 | Ask yourself this question: why do you think the Component should have re-rendered? There's two possible answers: 8 | - a ratom (used by the Component) changed 9 | - the props for (arguments to) the Component changed 10 | 11 | We'll deal with these two cases seperately. 12 | 13 | ### A Ratom Changed 14 | 15 | If a ratom changes but your Component doesn't update, then the gotchas to look out for are: 16 | 1. Make sure you are using a `reagent.core/atom` (i.e. a Reagent ratom) instead of a normal `clojure.core/atom`. Carefully check the `require` at the top of the `ns`. Components are only reactive with respect to Reagent ratoms. They don't react to changes in a Clojure atom. 17 | 2. Make sure you actually `deref` your ratom (e.g. `@app-state`) during the render function of your component. It is a common mistake for people to forget the leading `@`. Note that derefs that happen outside of the render function (such as during event handlers) do not make your component reactive to that ratom. 18 | 3. Make sure your ratom will survive a rerender. Either declare it as a global var, or use a form-2 or form-3 component. [Read this](https://github.com/reagent-project/reagent-cookbook/tree/master/basics/component-level-state) if you want to understand why. 19 | 4. If you put your ratom in a form-2 or form-3 component, be sure you are calling that function using `[square brackets]`, not `(parenthesis)`. 20 | When function (component) is called using `( )` Reagent doesn't create new component, but instead just places the function's return value into current component. In this case the function closure which should hold the local state doesn't work. 21 | 5. Make sure to `deref` your ratom outside of a seq or wrap that seq in a `doall`. See this [related issue](https://github.com/reagent-project/reagent/issues/18). 22 | 23 | ### Props Change 24 | 25 | If the props to a Component change, but it doesn't appear to re-render, then the cause will be this rookie mistake: you forgot to repeat the parameters in the inner, anonymous render function. 26 | 27 | ```clj 28 | (defn outer 29 | [a b c] ;; <--- parameters 30 | ;; .... 31 | (fn [a b c] ;; <--- forgetting to repeat parameters here is the mistake 32 | [:div 33 | (str a b c)])) 34 | ``` 35 | 36 | If you forget, the component renderer will stubbornly only ever render the 37 | original parameter values, not the updated ones, which can be baffling for 38 | a beginner. 39 | 40 | Remember, `outer` is called once per component instance. The parameters to `outer` 41 | will hold the initial parameter values. The inner renderer on the other hand, 42 | will be called by Reagent many times and, each time, potentially with alternative 43 | parameter values, but unless you repeat the parameters on the renderer it will 44 | close over those initial values in `outer`. As a result, the component renderer 45 | will stubbornly only ever render the original parameter values, not the updated ones. 46 | 47 | 48 | *** 49 | 50 | Up: [FAQ Index](../README.md)       51 | -------------------------------------------------------------------------------- /doc/FAQ/ForcingComponentRecreation.md: -------------------------------------------------------------------------------- 1 | # Forcing Component re-creation? 2 | 3 | ## React key with a stateful component 4 | 5 | React key can be used to provide identity for components even outside of lists. 6 | If the key changes, the component is recreated triggering all the implemented 7 | stateful methods (did-unmount, did-mount etc.): 8 | 9 | ```clj 10 | (defn comp [] 11 | [:div 12 | ^{:key dynamic-id} 13 | [stateful-component]]) 14 | ``` 15 | -------------------------------------------------------------------------------- /doc/FAQ/HtmlEntities.md: -------------------------------------------------------------------------------- 1 | # Using HTML entities 2 | 3 | React will escape HTML entities (like ` `, `×`) in the text elements. 4 | 5 | You could just use literal character: `×` or unicode code, which is converted to 6 | the character by Cljs compiler: `\u00D7`. 7 | 8 | HTML entities work in React JSX because JSX will unescape the entity code to 9 | literal character. 10 | 11 | You can do the same in ClojureScript by using `goog.string/unescapeEntities`: 12 | 13 | ```cljs 14 | (ns example 15 | (:require [goog.string :as gstr])) 16 | 17 | (defn comp [] 18 | [:h1 "Foo" (gstr/unescapeEntities "×")]) 19 | ``` 20 | 21 | Note: Yes, this can be inconvenient, but Reagent can't do this automatically as 22 | finding and replacing entities during runtime would be slow. 23 | -------------------------------------------------------------------------------- /doc/FAQ/MyAttributesAreMissing.md: -------------------------------------------------------------------------------- 1 | ### Question 2 | 3 | Why isn't my attribute `xyz` showing up on ? (where is `xyz` is something like `autoFocus`) 4 | 5 | ### Answer 6 | 7 | You might be spelling it incorrectly. 8 | 9 | React supports [camelCased HTML attributes](https://reactjs.org/docs/dom-elements.html#all-supported-html-attributes), 10 | but the equivalent in Reagent is dashed and lower cased. 11 | 12 | For example, with Reagent, you use `auto-focus`, instead of `autoFocus`. And 13 | you use `col-span` instead of React's `colSpan`. 14 | 15 | *** 16 | 17 | Up: [FAQ Index](../README.md)       18 | -------------------------------------------------------------------------------- /doc/FAQ/UsingAnEntity.md: -------------------------------------------------------------------------------- 1 | ### Question 2 | 3 | How can I use an entity like "nbsp"? 4 | 5 | ### Answer 6 | 7 | If you try to do this: 8 | ```clj 9 | [:div "hello" " " "there"] ;; <--- note: attempt to use an entity 10 | ``` 11 | then you will see the string for the entity. Which is not what you want. 12 | 13 | Instead you should do this: 14 | 15 | 1. Require in goog's string module... 16 | 17 | ```clj 18 | (:require [goog.string :as gstring]) 19 | ``` 20 | 21 | 2. Use it like this ... 22 | 23 | ```clj 24 | [:div "hello" (gstring/unescapeEntities " ") "there"] 25 | ``` 26 | 27 | **Note:** `unescapeEntities` relies on the DOM to produce a string with unescape entities; 28 | in `nodejs` the DOM is unlikely to be available (unless you try using 29 | [`jsdom`](https://www.npmjs.com/package/jsdom-global)). 30 | 31 | *** 32 | 33 | Up: [FAQ Index](../README.md)       34 | -------------------------------------------------------------------------------- /doc/FAQ/UsingRefs.md: -------------------------------------------------------------------------------- 1 | ### Question 2 | 3 | When using Reagent, how do I use React's `refs`? 4 | 5 | ### Answer 6 | 7 | Credit: this entry is entirely based on Paulus Esterhazy's [Reagent Mysteries series](https://presumably.de/reagent-mysteries-part-3-manipulating-the-dom.html) 8 | 9 | We'll start with a code fragment, because it is worth a 1000 words: 10 | 11 | ```cljs 12 | (defn video-ui [] 13 | (let [!video (clojure.core/atom nil)] ;; stores the 14 | (fn [{:keys [src]}] 15 | [:div 16 | [:div 17 | [:video {:src src 18 | :style {:width 400} 19 | :ref (fn [el] 20 | (reset! !video el))}]] 21 | [:div 22 | [:button {:on-click (fn [] 23 | (when-let [video @!video] ;; not nil? 24 | (if (.-paused video) 25 | (.play video) 26 | (.pause video))))} 27 | "Toogle"]]]))) 28 | ``` 29 | 30 | Notes: 31 | 1. This example uses a Form-2 component, which allows us to retain state outside of the renderer `fn`. The same technique would work with a Form-3 component. 32 | 2. We capture state in `!video`. In this example, the state we capture is a reference to a [video HTML element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video). 33 | 3. `!video` is a `clojure.core/atom` and not a `reagent.core/atom`. We use a normal Clojure `atom` because refs never change during the lifecycle of a component and if we used a reagent atom, it would cause an unnecessary re-render when the ref callback mutates the atom. 34 | 4. On the `:video` component there's a `:ref` callback function which establishes the state in `!video`. You can attach a ref callback to any of the Hiccup elements. 35 | 5. Thereafter, `@!video` is used with the `:button's` `:on-click` to manipulate the `video` DOM methods. 36 | 6. For full notes [read Paulus' blog post](https://presumably.de/reagent-mysteries-part-3-manipulating-the-dom.html) 37 | 7. For more background on callback refs, see [React's documentation](https://reactjs.org/docs/refs-and-the-dom.html) 38 | 39 | *** 40 | 41 | Up: [FAQ Index](../README.md)       42 | -------------------------------------------------------------------------------- /doc/FAQ/dangerouslySetInnerHTML.md: -------------------------------------------------------------------------------- 1 | ### Question 2 | 3 | How can I use React's [dangerouslySetInnerHTML](https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml) feature? 4 | 5 | ### Answer 6 | 7 | A minimal (contrived example): 8 | 9 | ```clj 10 | [:div 11 | {:dangerouslySetInnerHTML 12 | (r/unsafe-html "")}] 13 | ``` 14 | 15 | See [Security](../Security.md). 16 | 17 | *** 18 | 19 | Up: [FAQ Index](../README.md) 20 | -------------------------------------------------------------------------------- /doc/README.md: -------------------------------------------------------------------------------- 1 | ## Tutorials 2 | 3 | 1. [Using Hiccup to Describe HTML](UsingHiccupToDescribeHTML.md) 4 | 2. [Creating Reagent Components](CreatingReagentComponents.md) 5 | 3. [Using [square brackets] instead of (parentheses)](UsingSquareBracketsInsteadOfParens.md) 6 | 4. [When do components update?](WhenDoComponentsUpdate.md) 7 | 5. [[WIP] Managing State: atoms, cursors, Reactions, and tracking](ManagingState.md) 8 | 6. [Batching and Timing: How Reagent Renders Changes to Application State](BatchingAndTiming.md) 9 | 7. [Interop with React](InteropWithReact.md) 10 | 8. [React Features](ReactFeatures.md) and how to use them in Reagent 11 | 8. [Reagent Compiler](ReagentCompiler.md) 12 | 8. [Controlled Inputs](ControlledInputs.md) 13 | 14 | Also: 15 | * [purelyfunctional.tv ](https://purelyfunctional.tv/guide/reagent/) - an excellent, written tutorial 16 | * [Reagent Deep Dive Series by Timothy Pratley](http://timothypratley.blogspot.com.au/p/p.html) - a four part series 17 | * [Reagent Mysteries series by Paulus Esterhazy](https://presumably.de/) - a four part series 18 | * [Props, Children & Component Lifecycle](https://www.martinklepsch.org/posts/props-children-and-component-lifecycle-in-reagent.html) by Martin Klepsch 19 | * [Using Stateful JS Components - like D3](https://github.com/Day8/re-frame/blob/master/docs/Using-Stateful-JS-Components.md) (external link) 20 | 21 | ## Commercial Videos Series 22 | 23 | * [Learn Reagent Free](https://www.jacekschae.com/learn-reagent-free/tycit?coupon=REAGENT) 24 | * [Learn Reagent Pro](https://www.jacekschae.com/learn-reagent-pro/tycit?coupon=REAGENT) (Affiliate link, $30 discount) 25 | * [Learn Re-frame](https://www.jacekschae.com/learn-re-frame-pro?coupon=REAGENT) (Affiliate link, early access) 26 | * [purelyfunctional.tv ](https://purelyfunctional.tv/guide/reagent/) 27 | * [Lambda Island Videos](https://lambdaisland.com/collections/react-reagent-re-frame) 28 | 29 | ## Frequently Asked Questions 30 | 31 | 1. [Why isn't my Component re-rendering?](FAQ/ComponentNotRerendering.md) 32 | 1. [How do I use React's "refs"](FAQ/UsingRefs.md) 33 | 2. [How can I use an entity like "nbsp"?](FAQ/UsingAnEntity.md) 34 | 3. [Why is my attribute (like autoFocus) missing?](FAQ/MyAttributesAreMissing.md) 35 | 4. [How can I use React's dangerouslySetInnerHTML?](FAQ/dangerouslySetInnerHTML.md) 36 | 5. [Reagent doesn't work after updating dependencies](FAQ/CljsjsReactProblems.md) 37 | 5. [How do I force Component re-creation?](FAQ/ForcingComponentRecreation.md) 38 | 5. [Using HTML entities](FAQ/HtmlEntities.md) 39 | 6. [How do I access "props" in lifecycle methods?](http://nils-blum-oeste.net/clojurescripts-reagent-using-props-in-lifecycle-hooks/) (external link) 40 | 41 | ## Examples 42 | 43 | - [MaterialUI v1 with working TextField](examples/material-ui.md), React interop example 44 | - [React-sortable-hoc](../examples/react-sortable-hoc/src/example/core.cljs), React interop example 45 | 46 | 47 | ### Want To Add An FAQ? 48 | 49 | Many Thanks!! We'd like that: 50 | 1. As a base, just use the structure from one of the existing FAQs files 51 | 2. Give us a PR which includes your new file AND a change to this README so your entry is listed. 52 | 53 | #### Misc Docs 54 | 55 | - [0.8-upgrade](0.8-upgrade.md) 56 | - [development](development.md) 57 | -------------------------------------------------------------------------------- /doc/ReactRenderers.md: -------------------------------------------------------------------------------- 1 | # React Renderers 2 | 3 | React itself is not limited to HTML and the DOM. 4 | [ReactDOM](https://react.dev/reference/react-dom) is one implementation that 5 | renders React elements into the DOM and HTML. 6 | 7 | Reagent does not have test cases for other renderers, but there should be 8 | nothing in its implementation that is specifically tied to the DOM or HTML, 9 | meaning it should work with other rendering targets. 10 | 11 | Reagent only requires the `react-dom` module in the `reagent.dom`, 12 | `reagent.dom.client`, and `reagent.dom.server` namespaces. This means you can 13 | use `reagent.core` with other targets without unintentionally requiring 14 | `react-dom`. 15 | 16 | ## Other Rendering Targets 17 | 18 | One of the most common alternative targets is [React 19 | Native](https://reactnative.dev/). Reagent has been successfully used with 20 | React Native applications for years. 21 | 22 | Reagent does not provide built-in `render` functions for non-DOM targets, so 23 | you will need to handle rendering manually. This is typically done using 24 | `reagent.core/as-element` to convert a Reagent Hiccup form into a React element 25 | for use with a renderer, or `reagent.core/reactify-component` to turn a 26 | function into a React component. 27 | 28 | Example: 29 | 30 | ```clj 31 | (ns app.main 32 | (:require ["react-native" :refer [View Text AppRegistry]])) 33 | 34 | (defn root [] 35 | [:> View [:> Text "Hello world"]]) 36 | 37 | ;; Using reactify-component to create a React component 38 | (def Root (r/reactify-component root)) 39 | 40 | ;; Using as-element, where the function itself returns a React element 41 | (defn Root [] 42 | (r/as-element [root])) 43 | 44 | ;; React Native entry point 45 | (defn init [] 46 | (.registerComponent AppRegistry "App" (fn [] Root))) 47 | ``` 48 | 49 | ### Other Examples of Alternative Render Targets 50 | 51 | - [react-three-fiber](https://r3f.docs.pmnd.rs/getting-started/introduction) (for 3D rendering) 52 | - [Ink](https://github.com/vadimdemedes/ink) (for rendering React components in the terminal) 53 | 54 | ## Reporting Issues 55 | 56 | If you encounter issues with other rendering targets, consider opening an 57 | issue. Providing a minimal example repository that reproduces the problem will 58 | be extremely helpful, as the Reagent project does not include examples for all 59 | possible render targets. 60 | -------------------------------------------------------------------------------- /doc/ReagentCompiler.md: -------------------------------------------------------------------------------- 1 | # Reagent Compiler 2 | 3 | Reagent Compiler object is a new way to configure how Reagent 4 | turns the Hiccup-style markup into React components and elements. 5 | 6 | As a first step, this can be used to turn on option to create 7 | functional components when a function is referred in a Hiccup vector: 8 | `[component-fn parameters]`. 9 | 10 | Read more about Hooks 11 | 12 | ```cljs 13 | (def functional-compiler (reagent.core/create-compiler {:function-components true})) 14 | 15 | ;; Using the option 16 | (reagent.dom/render [main] div functional-compiler) 17 | (reagent.core/as-element [main] functional-compiler) 18 | ;; Setting compiler as the default 19 | (reagent.core/set-default-compiler! functional-compiler) 20 | ``` 21 | 22 | ## Functional components implementation 23 | 24 | Features: 25 | 26 | - Ratoms works. 27 | - The functions are wrapped in another function, which uses two 28 | state hooks to store component identity and "update count" - which is used to 29 | force re-render when Ratoms the component uses are updated. 30 | - The functions is wrapped in `react/memo` to implement logic similar to 31 | `shouldComponentUpdate` (component is rendered only if the properties change). 32 | - This implementation passes the same test suite as class components. 33 | 34 | Differences to Class component implementation: 35 | 36 | - `reagent.dom/render` doesn't return the Component instance, but just `nil` 37 | - `reagent.core/current-component` returns a mocked object that can be passed to `reagent.core/force-update`, 38 | but won't support everything that real Component instance would support. 39 | - A bit slower compared to Class component implementation 40 | - `useEffect` cleanup function is called asynchronously some time after 41 | unmounting the component from DOM (in React 17). This is used to dispose component RAtom, 42 | which will affect e.g. `r/with-let` `finally` function being called. Cleanup 43 | is still called before the component is mounted again. This probably shouldn't 44 | affect any real use cases, but required waiting two animation frames on 45 | Reagent tests to assert that the `finally` was ran. 46 | ([More information](https://reactjs.org/blog/2020/08/10/react-v17-rc.html#effect-cleanup-timing)) 47 | - Using `r/wrap` as component parameter seems to in some cases re-render 48 | components when source atom is changed, even if the value in path didn't 49 | change. Could be related to how `react/memo` handles changes properties. 50 | 51 | ![1.0.0-alpha2 benchmark](benchmark.png) 52 | 53 | (Local test run with https://github.com/krausest/js-framework-benchmark, with added function component case) 54 | 55 | ## Reasoning 56 | 57 | Now that this mechanism to control how Reagent compiles Hiccup-style markup 58 | to React calls is in place, it will be probably used later to control 59 | some other things also: 60 | 61 | From [Clojurist Together announcenment](https://www.clojuriststogether.org/news/q1-2020-funding-announcement/): 62 | 63 | > As this [hooks] affects how Reagent turns Hiccup to React elements and components, I 64 | > have some ideas on allowing users configure the Reagent Hiccup compiler, 65 | > similar to what [Hicada](https://github.com/rauhs/hicada) does. This would also allow introducing optional 66 | > features which would break existing Reagent code, by making users opt-in to 67 | > these. One case would be to make React component interop simpler. 68 | 69 | Some ideas: 70 | 71 | - Providing options to control how component parameters are converted to JS 72 | objects (or even disable automatic conversion) 73 | - Implement support for custom tags (if you can provide your own function 74 | to create element from a keyword, this will be easy) 75 | 76 | Open questions: 77 | 78 | - Will this cause problems for libraries? Do the libraries have to start 79 | calling `as-element` with their own Compiler to ensure compatibility. 80 | -------------------------------------------------------------------------------- /doc/Security.md: -------------------------------------------------------------------------------- 1 | # Security and Reagent 2 | 3 | ## Hiccup Data 4 | 5 | Reagent uses Clojure data structures to represent the React elements that will 6 | be created and mounted on the page. 7 | 8 | In most cases, React does its best to avoid code injection attacks by making it 9 | difficult to accidentally create elements that execute JavaScript code. For 10 | example, ` 13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/functional-components-and-hooks/shadow-cljs.edn: -------------------------------------------------------------------------------- 1 | {:source-paths ["src"] 2 | :dev-http {3000 ["resources/public" "target/public"]} 3 | :builds 4 | {:app {:target :browser 5 | :output-dir "target/public/js" 6 | :asset-path "/js" 7 | :modules {:main {:entries [example.core]}}}} 8 | :dependencies [[reagent "1.2.0"]]} 9 | -------------------------------------------------------------------------------- /examples/functional-components-and-hooks/src/example/core.cljs: -------------------------------------------------------------------------------- 1 | (ns example.core 2 | (:require [reagent.core :as r] 3 | [reagent.dom :as rdom] 4 | [clojure.string :as str] 5 | ["react" :as react])) 6 | 7 | ;; Same as simpleexample, but uses Hooks instead of Ratoms 8 | 9 | (defn greeting [message] 10 | [:h1 message]) 11 | 12 | (defn clock [time-color] 13 | (let [[timer update-time] (react/useState (js/Date.)) 14 | time-str (-> timer .toTimeString (str/split " ") first)] 15 | (react/useEffect 16 | (fn [] 17 | (let [i (js/setInterval #(update-time (js/Date.)) 1000)] 18 | (fn [] 19 | (js/clearInterval i))))) 20 | [:div.example-clock 21 | {:style {:color time-color}} 22 | time-str])) 23 | 24 | (defn color-input [time-color update-time-color] 25 | [:div.color-input 26 | "Time color: " 27 | [:input {:type "text" 28 | :value time-color 29 | :on-change #(update-time-color (-> % .-target .-value))}]]) 30 | 31 | (defn simple-example [] 32 | (let [[time-color update-time-color] (react/useState "#f34")] 33 | [:div 34 | [greeting "Hello world, it is now"] 35 | [clock time-color] 36 | [color-input time-color update-time-color] 37 | 38 | ;; Or with the default options you can create function components using :f> shortcut: 39 | #_#_#_ 40 | [greeting "Hello world, it is now"] 41 | [:f> clock time-color] 42 | [:f> color-input time-color update-time-color] 43 | ])) 44 | 45 | (def functional-compiler (r/create-compiler {:function-components true})) 46 | 47 | (defn run [] 48 | (rdom/render [simple-example] (js/document.getElementById "app") functional-compiler) 49 | ;; Or with default options and shortcut: 50 | #_ 51 | (rdom/render [:f> simple-example] (js/document.getElementById "app")) 52 | ) 53 | 54 | (run) 55 | -------------------------------------------------------------------------------- /examples/geometry/README.md: -------------------------------------------------------------------------------- 1 | # Reagent example app 2 | 3 | `npx shadow-cljs watch app` 4 | -------------------------------------------------------------------------------- /examples/geometry/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-context-example", 3 | "dependencies": { 4 | "react": "18.3.1", 5 | "react-dom": "18.3.1" 6 | }, 7 | "devDependencies": { 8 | "shadow-cljs": "2.28.20" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/geometry/resources/public/example.css: -------------------------------------------------------------------------------- 1 | 2 | div, h1, input { 3 | font-family: HelveticaNeue, Helvetica; 4 | color: #777; 5 | } 6 | -------------------------------------------------------------------------------- /examples/geometry/resources/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Example 6 | 7 | 8 | 9 |
10 |

Reagent example app – see README.md

11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/geometry/shadow-cljs.edn: -------------------------------------------------------------------------------- 1 | {:source-paths ["src"] 2 | :dev-http {3000 ["resources/public" "target/public"]} 3 | :builds 4 | {:app {:target :browser 5 | :output-dir "target/public/js" 6 | :asset-path "/js" 7 | :devtools {:after-load geometry.core/run} 8 | :modules {:main {:entries [geometry.core] 9 | :init-fn geometry.core/run}}}} 10 | :dependencies [[reagent "1.2.0"]]} 11 | -------------------------------------------------------------------------------- /examples/geometry/src/geometry/components.cljs: -------------------------------------------------------------------------------- 1 | (ns geometry.components 2 | (:require [goog.events :as events] 3 | [geometry.geometry :refer [x y dist] :as g]) 4 | (:import [goog.events EventType])) 5 | 6 | (def point-defaults 7 | {:stroke "black" 8 | :stroke-width 2 9 | :fill "blue" 10 | :r 5}) 11 | 12 | (def segment-defaults 13 | {:stroke "black" 14 | :stroke-width 2}) 15 | 16 | (def circle-defaults 17 | {:fill "rgba(255,0,0,0.1)" 18 | :stroke "black" 19 | :stroke-width 2}) 20 | 21 | (def rect-defaults 22 | {:stroke "black" 23 | :width 10 24 | :height 30}) 25 | 26 | (defn drag-move-fn [on-drag] 27 | (fn [evt] 28 | (on-drag (.-clientX evt) (.-clientY evt)))) 29 | 30 | (defn drag-end-fn [drag-move drag-end on-end] 31 | (fn [evt] 32 | (events/unlisten js/window EventType.MOUSEMOVE drag-move) 33 | (events/unlisten js/window EventType.MOUSEUP @drag-end) 34 | (on-end))) 35 | 36 | (defn dragging 37 | ([on-drag] (dragging on-drag (fn []) (fn []))) 38 | ([on-drag on-start on-end] 39 | (let [drag-move (drag-move-fn on-drag) 40 | drag-end-atom (atom nil) 41 | drag-end (drag-end-fn drag-move drag-end-atom on-end)] 42 | (on-start) 43 | (reset! drag-end-atom drag-end) 44 | (events/listen js/window EventType.MOUSEMOVE drag-move) 45 | (events/listen js/window EventType.MOUSEUP drag-end)))) 46 | 47 | (defn point [{:keys [on-drag]} p] 48 | [:circle 49 | (merge point-defaults 50 | {:on-mouse-down #(dragging on-drag) 51 | :cx (x p) 52 | :cy (y p)})]) 53 | 54 | (defn segment [from to] 55 | [:line 56 | (merge segment-defaults 57 | {:x1 (x from) :y1 (y from) 58 | :x2 (x to) :y2 (y to)})]) 59 | 60 | (defn triangle [a b c] 61 | [:g 62 | [segment a b] 63 | [segment b c] 64 | [segment c a]]) 65 | 66 | (defn circle [c r] 67 | [:circle 68 | (merge circle-defaults 69 | {:cx (x c) 70 | :cy (y c) 71 | :r (dist c r)})]) 72 | 73 | (defn rect [{:keys [on-drag on-start on-end]} c] 74 | [:rect (merge rect-defaults 75 | {:on-mouse-down #(dragging on-drag on-start on-end) 76 | :x (x c) 77 | :y (- (y c) 15)})]) 78 | -------------------------------------------------------------------------------- /examples/geometry/src/geometry/core.cljs: -------------------------------------------------------------------------------- 1 | (ns geometry.core 2 | (:require ["react" :as react] 3 | [geometry.components :as c] 4 | [geometry.geometry :as g] 5 | [reagent.core :as r] 6 | [reagent.dom :as rdom])) 7 | 8 | (enable-console-print!) 9 | 10 | (defonce points 11 | (r/atom 12 | {:p1 (g/point 100 100) 13 | :p2 (g/point 200 200) 14 | :p3 (g/point 100 200) 15 | :c (g/point 250 250) 16 | :p (g/point 250 300)})) 17 | 18 | (defonce slider 19 | (r/atom 20 | {:handle (g/point 500 50) 21 | :history []})) 22 | 23 | (defn record-state [_ _ _ s] 24 | (swap! slider (fn [{:keys [history] :as coll}] 25 | (assoc coll :history (conj history s))))) 26 | 27 | (defn start-recording-history [] 28 | (let [history (:history @slider)] 29 | (add-watch points :record record-state))) 30 | 31 | (defn stop-recording-history [] 32 | (remove-watch points :record)) 33 | 34 | (add-watch points :record record-state) 35 | 36 | (defn get-bcr [svg-ref] 37 | (.. svg-ref 38 | -current 39 | getBoundingClientRect)) 40 | 41 | (defn move-point [svg-ref p] 42 | (fn [x y] 43 | (let [bcr (get-bcr svg-ref)] 44 | (swap! points assoc p (g/point (- x (.-left bcr)) (- y (.-top bcr))))))) 45 | 46 | (defn move-slider [svg-ref p] 47 | (fn [x y] 48 | (let [new-x (-> (- x (.-left (get-bcr svg-ref))) 49 | (min 500) 50 | (max 100)) 51 | position (/ (- new-x 100) 52 | (- 500 100)) 53 | history (:history @slider) 54 | history-points (nth history (int (* (dec (count history)) position)) nil)] 55 | (swap! slider assoc p (g/point new-x 50)) 56 | (if history-points 57 | (reset! points history-points))))) 58 | 59 | (defn root [svg-ref] 60 | (let [{:keys [p1 p2 p3 p c]} @points] 61 | [:g 62 | [c/triangle p1 p2 p3] 63 | [c/circle p c] 64 | [c/segment p c] 65 | [c/segment (g/point 100 50) (g/point 500 50)] 66 | [c/rect {:on-drag (move-slider svg-ref :handle) 67 | :on-start stop-recording-history 68 | :on-end start-recording-history} (:handle @slider)] 69 | [c/point {:on-drag (move-point svg-ref :c)} c] 70 | [c/point {:on-drag (move-point svg-ref :p)} p] 71 | [c/point {:on-drag (move-point svg-ref :p1)} p1] 72 | [c/point {:on-drag (move-point svg-ref :p2)} p2] 73 | [c/point {:on-drag (move-point svg-ref :p3)} p3]])) 74 | 75 | (defn main* [{:keys [width height]}] 76 | (let [svg-ref (react/useRef nil)] 77 | [:svg 78 | {:ref svg-ref 79 | :width (or width 800) 80 | :height (or height 600) 81 | :style {:border "1px solid black"}} 82 | [:text {:style {:-webkit-user-select "none" 83 | :-moz-user-select "none"} 84 | :x 20 :y 20 :font-size 20} 85 | "The points are draggable and the slider controls history"] 86 | [root svg-ref]])) 87 | 88 | (defn main 89 | "Just to keep main fn uses regular component uses, without :f>" 90 | [props] 91 | [:f> main* props]) 92 | 93 | (defn by-id [id] 94 | (.getElementById js/document id)) 95 | 96 | (defn ^:export run [] 97 | (rdom/render [main] (by-id "app"))) 98 | -------------------------------------------------------------------------------- /examples/geometry/src/geometry/geometry.cljs: -------------------------------------------------------------------------------- 1 | (ns geometry.geometry) 2 | 3 | (defprotocol IPoint 4 | (x [p]) 5 | (y [p])) 6 | 7 | (deftype Point [x-coord y-coord] 8 | IPoint 9 | (x [_] x-coord) 10 | (y [_] y-coord)) 11 | 12 | (defn point [x y] 13 | (->Point x y)) 14 | 15 | (defn dist [p1 p2] 16 | (js/Math.sqrt (+ (js/Math.pow (- (x p2) (x p1)) 2) 17 | (js/Math.pow (- (y p2) (y p1)) 2)))) 18 | 19 | -------------------------------------------------------------------------------- /examples/material-ui/README.md: -------------------------------------------------------------------------------- 1 | # Material-ui example 2 | 3 | `npx shadow-cljs watch client` 4 | -------------------------------------------------------------------------------- /examples/material-ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "material-ui", 3 | "dependencies": { 4 | "@mui/icons-material": "5.8.0", 5 | "@mui/lab": "5.0.0-alpha.96", 6 | "@mui/material": "5.8.0", 7 | "@mui/styles": "5.8.0", 8 | "react": "18.3.1", 9 | "react-dom": "18.3.1" 10 | }, 11 | "devDependencies": { 12 | "shadow-cljs": "2.28.20" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/material-ui/resources/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Example 6 | 7 | 8 |
9 |

Reagent example app – see README.md

10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/material-ui/shadow-cljs.edn: -------------------------------------------------------------------------------- 1 | {:builds 2 | {:app {:asset-path "/js" 3 | :modules {:main {:entries [example.core]}} 4 | :output-dir "target/public/js" 5 | :target :browser}} 6 | :dependencies [[reagent "1.2.0"]] 7 | :dev-http {3000 ["resources/public" "target/public"]} 8 | :nrepl {:port 3333} 9 | :source-paths ["src"]} 10 | -------------------------------------------------------------------------------- /examples/react-context/README.md: -------------------------------------------------------------------------------- 1 | # React Context 2 | 3 | `npx shadow-cljs watch app` 4 | -------------------------------------------------------------------------------- /examples/react-context/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-context-example", 3 | "dependencies": { 4 | "react": "18.3.1", 5 | "react-dom": "18.3.1" 6 | }, 7 | "devDependencies": { 8 | "shadow-cljs": "2.28.20" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/react-context/resources/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Example 6 | 7 | 8 |
9 |

Reagent example app – see README.md

10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/react-context/shadow-cljs.edn: -------------------------------------------------------------------------------- 1 | {:source-paths ["src"] 2 | :dev-http {3000 ["resources/public" "target/public"]} 3 | :builds 4 | {:app {:target :browser 5 | :output-dir "target/public/js" 6 | :asset-path "/js" 7 | :modules {:main {:entries [example.core]}}}} 8 | :dependencies [[reagent "1.2.0"]]} 9 | -------------------------------------------------------------------------------- /examples/react-context/src/example/core.cljs: -------------------------------------------------------------------------------- 1 | (ns example.core 2 | (:require [reagent.core :as r] 3 | [reagent.dom :as rdom] 4 | [react :as react])) 5 | 6 | (defonce my-context (react/createContext "default")) 7 | 8 | (def Provider (.-Provider my-context)) 9 | (def Consumer (.-Consumer my-context)) 10 | 11 | (defn children [] 12 | [:> Consumer {} 13 | (fn [v] 14 | (r/as-element [:div "Context: " (pr-str v)]))]) 15 | 16 | (defn root [] 17 | ;; Provider takes props with single property, value 18 | ;; :< or adapt-react-class converts the Cljs properties 19 | ;; map to JS object for the Provider component. 20 | [:div 21 | [:> Provider {:value "bar"} 22 | [children]] 23 | 24 | ;; :> and adapt-react-class convert the props 25 | ;; recursively to JS objects, so this might not be 26 | ;; what you want. 27 | [:> Provider {:value {:foo "bar"}} 28 | [children]] 29 | 30 | ;; To yourself control how the props are handled, 31 | ;; use create-element directly. 32 | ;; Properties map needs to be JS object here, but now the 33 | ;; value is used as is, Cljs map. 34 | ;; Note that you need to convert children from Reagent markup 35 | ;; to React elements yourself. 36 | (r/create-element Provider 37 | #js {:value {:foo "bar"}} 38 | (r/as-element [children]))]) 39 | 40 | (defn start [] 41 | (rdom/render [root] (js/document.getElementById "app"))) 42 | 43 | (start) 44 | -------------------------------------------------------------------------------- /examples/react-mde/README.md: -------------------------------------------------------------------------------- 1 | # Example of using [ReactMde](https://github.com/andrerpena/react-mde) with Reagent. 2 | 3 | This example shows how we can use [react-mde](https://github.com/andrerpena/react-mde) with 4 | reagent, taking into account the needed fix for the `textarea` component used by `react-mde`. 5 | 6 | This example is very similar to the `material-ui` example, but also requires the usage of 7 | `forwardRef` with the custom textarea component. 8 | 9 | 10 | ### Running 11 | 12 | ```sh 13 | npm install 14 | npm run dev 15 | ``` 16 | -------------------------------------------------------------------------------- /examples/react-mde/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reagent-react-mde-example", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "author": "", 6 | "dependencies": { 7 | "react": "18.3.1", 8 | "react-dom": "18.3.1", 9 | "react-markdown": "9.0.3", 10 | "react-mde": "11.5.0" 11 | }, 12 | "devDependencies": { 13 | "shadow-cljs": "2.28.20" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/react-mde/resources/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Example 6 | 7 | 8 | 9 |
10 |

Reagent example app – see README.md

11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/react-mde/resources/public/react-mds-all.css: -------------------------------------------------------------------------------- 1 | ../../node_modules/react-mde/lib/styles/css/react-mde-all.css -------------------------------------------------------------------------------- /examples/react-mde/shadow-cljs.edn: -------------------------------------------------------------------------------- 1 | {:builds 2 | {:app {:asset-path "/js" 3 | :modules {:main {:entries [example.core]}} 4 | :output-dir "target/public/js" 5 | :target :browser}} 6 | :dependencies [[reagent "1.2.0"]] 7 | :dev-http {3000 ["resources/public" "target/public"]} 8 | :nrepl {:port 3333} 9 | :source-paths ["src"]} 10 | -------------------------------------------------------------------------------- /examples/react-mde/src/example/core.cljs: -------------------------------------------------------------------------------- 1 | (ns example.core 2 | "This example shows how to use the `react-mde` library, which provides a markdown editor as 3 | a React component. 4 | The integration is not straightforward because we need to provide a custom `textarea` 5 | component to `ReactMde` so we can have the cursor positining fixes needed by reagent. BUT 6 | we must make sure that `ReactMde` sees a ref to the true `textarea` component, and not reagent's 7 | wrapper." 8 | (:require [reagent.core :as r] 9 | [reagent.dom :as rdom] 10 | ["react-mde$default" :as ReactMde] 11 | ["react-markdown" :as ReactMarkdown] 12 | [react :as react] 13 | ;; FIXME: Internal impl namespace should not be used 14 | [reagent.impl.template :as rtpl])) 15 | 16 | 17 | ;; 18 | ;; Constants and helpers 19 | ;; 20 | (def common-style {:width "700px" :margin "60px auto"}) 21 | 22 | (defn render-markdown 23 | "Uses ReactMarkdown to render the markdown. Returns a `Promise` as expected by `ReactMde`." 24 | [source] 25 | (js/Promise. 26 | (fn [resolve _] 27 | (resolve 28 | (r/as-element 29 | [:> ReactMarkdown {:source source}]))))) 30 | 31 | 32 | ;; 33 | ;; Two broken examples that won't work properly. 34 | ;; 35 | (defn without-any-fix 36 | "Renders a ReactMde component without any fix for textarea component." 37 | [] 38 | (let [state (r/atom {:value "Initial Value!" :tab "write"})] 39 | (fn [] 40 | [:div {:style common-style} 41 | [:h3 "Without any fix"] 42 | [:i "Cursor goes to the end at every keystroke."] 43 | [:> ReactMde {:value (:value @state) 44 | :on-change #(swap! state assoc :value %) 45 | :selected-tab (:tab @state) 46 | :on-tab-change #(swap! state assoc :tab %) 47 | :generate-markdown-preview #(render-markdown %)}] 48 | [:div "Value: " (:value @state)]]))) 49 | 50 | (defn with-custom-textarea-but-no-forward-ref 51 | "Renders a ReactMde component with a custom textarea, but without `forwardRef`" 52 | [] 53 | (let [state (r/atom {:value "Initial Value!" :tab "write"}) 54 | textarea (r/create-class {:reagent-render (fn [props] [:textarea props])})] 55 | (fn [] 56 | [:div {:style common-style} 57 | [:h3 "With custom textarea but no forwardRef"] 58 | [:i "Cursor behaves fine, but the functionalities from the toolbar are broken."] 59 | [:> ReactMde {:value (:value @state) 60 | :on-change #(swap! state assoc :value %) 61 | :selected-tab (:tab @state) 62 | :on-tab-change #(swap! state assoc :tab %) 63 | :generate-markdown-preview #(render-markdown %) 64 | :text-area-component textarea}] 65 | [:div "Value: " (:value @state)]]))) 66 | 67 | 68 | ;; 69 | ;; Real working example 70 | ;; 71 | (def textarea-component 72 | "This is a trick needed so that the `textarea` component used by the `ReactMde` works. 73 | 1. Use reagent's custom `textarea` component, instead of a plain `textarea`. 74 | 2. Use ForwardRef to ensure that the ref seen by ReactMde points to the plain `textarea` 75 | instead of reagent wrapper." 76 | (react/forwardRef 77 | (fn textarea [props ref] 78 | (let [props (assoc (js->clj props) :ref ref)] 79 | (r/as-element [:textarea props]))))) 80 | 81 | 82 | (defn react-mde 83 | "Wrapper around ReactMde using our custom textarea-component" 84 | [props] 85 | ;; FIXME: Internal fn should not be used 86 | (let [props (rtpl/convert-prop-value (assoc props :text-area-component textarea-component))] 87 | (r/create-element ReactMde props))) 88 | 89 | 90 | (defn working-example [] 91 | (let [state (r/atom {:value "Initial Value!" :tab "write"})] 92 | (fn [] 93 | [:div {:style common-style} 94 | [:h3 "Working example!"] 95 | [react-mde {:value (:value @state) 96 | :on-change #(swap! state assoc :value %) 97 | :selected-tab (:tab @state) 98 | :on-tab-change #(swap! state assoc :tab %) 99 | :generate-markdown-preview #(render-markdown %)}] 100 | [:div "Value: " (:value @state)]]))) 101 | 102 | 103 | (defn main [] 104 | [:<> 105 | [without-any-fix] 106 | [with-custom-textarea-but-no-forward-ref] 107 | [working-example]]) 108 | 109 | (defn start [] 110 | (rdom/render [main] (js/document.getElementById "app"))) 111 | 112 | (start) 113 | -------------------------------------------------------------------------------- /examples/react-sortable-hoc/README.md: -------------------------------------------------------------------------------- 1 | # React sortable HOC example 2 | 3 | `npx shadow-cljs watch app` 4 | -------------------------------------------------------------------------------- /examples/react-sortable-hoc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-context-example", 3 | "dependencies": { 4 | "react": "18.3.1", 5 | "react-dom": "18.3.1", 6 | "react-sortable-hoc": "^2.0.0" 7 | }, 8 | "devDependencies": { 9 | "shadow-cljs": "2.28.20" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/react-sortable-hoc/resources/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Example 6 | 7 | 8 |
9 |

Reagent example app – see README.md

10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/react-sortable-hoc/shadow-cljs.edn: -------------------------------------------------------------------------------- 1 | {:source-paths ["src"] 2 | :dev-http {3000 ["resources/public" "target/public"]} 3 | :builds 4 | {:app {:target :browser 5 | :output-dir "target/public/js" 6 | :asset-path "/js" 7 | :modules {:main {:entries [example.core]}}}} 8 | :dependencies [[reagent "1.2.0"]]} 9 | -------------------------------------------------------------------------------- /examples/react-sortable-hoc/src/example/core.cljs: -------------------------------------------------------------------------------- 1 | (ns example.core 2 | (:require [reagent.core :as r] 3 | [reagent.dom :as rdom] 4 | [react-sortable-hoc :as sort])) 5 | 6 | ;; Adapted from https://github.com/clauderic/react-sortable-hoc/blob/master/examples/drag-handle.js#L10 7 | 8 | (def DragHandle 9 | (sort/SortableHandle. 10 | ;; Alternative to r/reactify-component, which doens't convert props and hiccup, 11 | ;; is to just provide fn as component and use as-element or create-element 12 | ;; to return React elements from the component. 13 | (fn [] 14 | (r/as-element [:span "::"])))) 15 | 16 | (def SortableItem 17 | (sort/SortableElement. 18 | (r/reactify-component 19 | (fn [{:keys [value]}] 20 | [:li 21 | [:> DragHandle] 22 | value])))) 23 | 24 | ;; Alternative without reactify-component 25 | ;; props is JS object here 26 | #_ 27 | (def SortableItem 28 | (sort/SortableElement. 29 | (fn [props] 30 | (r/as-element 31 | [:li 32 | [:> DragHandle] 33 | (.-value props)])))) 34 | 35 | (def SortableList 36 | (sort/SortableContainer. 37 | (r/reactify-component 38 | (fn [{:keys [items]}] 39 | [:ul 40 | (for [[value index] (map vector items (range))] 41 | ;; No :> or adapt-react-class here because that would convert value to JS 42 | (r/create-element 43 | SortableItem 44 | #js {:key (str "item-" index) 45 | :index index 46 | :value value}))])))) 47 | 48 | ;; Or using new :r> shortcut, which doesn't do props conversion 49 | #_ 50 | (def SortableList 51 | (sort/SortableContainer. 52 | (r/reactify-component 53 | (fn [{:keys [items]}] 54 | [:ul 55 | (for [[value index] (map vector items (range))] 56 | [:r> SortableItem 57 | #js {:key (str "item-" index) 58 | :index index 59 | :value value}])])))) 60 | 61 | (defn vector-move [coll prev-index new-index] 62 | (let [items (into (subvec coll 0 prev-index) 63 | (subvec coll (inc prev-index)))] 64 | (-> (subvec items 0 new-index) 65 | (conj (get coll prev-index)) 66 | (into (subvec items new-index))))) 67 | 68 | (comment 69 | (= [0 2 3 4 1 5] (vector-move [0 1 2 3 4 5] 1 4))) 70 | 71 | (defn sortable-component [] 72 | (let [items (r/atom (vec (map (fn [i] (str "Item " i)) (range 6))))] 73 | (fn [] 74 | (r/create-element 75 | SortableList 76 | #js {:items @items 77 | :onSortEnd (fn [event] 78 | (swap! items vector-move (.-oldIndex event) (.-newIndex event))) 79 | :useDragHandle true})))) 80 | 81 | (defn main [] 82 | [sortable-component]) 83 | 84 | (defn start [] 85 | (rdom/render [main] (js/document.getElementById "app"))) 86 | 87 | (start) 88 | -------------------------------------------------------------------------------- /examples/react-transition-group/README.md: -------------------------------------------------------------------------------- 1 | # React transition group example 2 | 3 | `npx shadow-cljs watch app` 4 | -------------------------------------------------------------------------------- /examples/react-transition-group/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-context-example", 3 | "dependencies": { 4 | "react": "18.3.1", 5 | "react-dom": "18.3.1", 6 | "react-transition-group": "^4.4.5" 7 | }, 8 | "devDependencies": { 9 | "shadow-cljs": "2.28.20" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/react-transition-group/resources/public/fade.css: -------------------------------------------------------------------------------- 1 | .hide { 2 | opacity: 0.01; 3 | } 4 | 5 | .fade-enter { 6 | opacity: 0.01; 7 | } 8 | 9 | .fade-enter.fade-enter-active { 10 | opacity: 1; 11 | transition: opacity 500ms ease-in; 12 | } 13 | 14 | .fade-exit { 15 | opacity: 1; 16 | } 17 | 18 | .fade-exit.fade-exit-active { 19 | opacity: 0.01; 20 | transition: opacity 300ms ease-in; 21 | } 22 | -------------------------------------------------------------------------------- /examples/react-transition-group/resources/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Example 6 | 7 | 8 | 9 |
10 |

Reagent example app – see README.md

11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/react-transition-group/shadow-cljs.edn: -------------------------------------------------------------------------------- 1 | {:source-paths ["src"] 2 | :dev-http {3000 ["resources/public" "target/public"]} 3 | :builds 4 | {:app {:target :browser 5 | :output-dir "target/public/js" 6 | :asset-path "/js" 7 | :modules {:main {:entries [example.core]}}}} 8 | :dependencies [[reagent "1.2.0"]]} 9 | -------------------------------------------------------------------------------- /examples/react-transition-group/src/example/core.cljs: -------------------------------------------------------------------------------- 1 | (ns example.core 2 | (:require [reagent.core :as r] 3 | [reagent.dom :as rdom] 4 | ["react-transition-group" :refer [TransitionGroup CSSTransition]] 5 | [goog.object :as gobj])) 6 | 7 | (defn transition-group-example [] 8 | (let [state (r/atom {:i 1 9 | :elements []})] 10 | (fn [] 11 | [:div 12 | [:button 13 | {:on-click (fn [_] 14 | (swap! state (fn [s] 15 | (-> s 16 | (update :i inc) 17 | (update :elements conj (inc (:i s)))))))} 18 | "Append element"] 19 | [:button 20 | {:on-click (fn [_] 21 | (swap! state (fn [s] 22 | (-> s 23 | (update :elements #(vec (drop-last %)))))))} 24 | "Remove element"] 25 | [:> TransitionGroup 26 | {:component "ul"} 27 | (for [e (:elements @state)] 28 | ;; Can't move this to separate function, or reagent will add component in between and transitions break 29 | [:> CSSTransition 30 | {:key e 31 | :classNames "fade" 32 | :timeout 500 33 | :on-enter #(js/console.log "enter" e) 34 | :on-entering #(js/console.log "entering" e) 35 | :on-entered #(js/console.log "entered" e) 36 | :on-exit #(js/console.log "enter" e) 37 | :on-exiting #(js/console.log "exiting" e) 38 | :on-exited #(js/console.log "exited" e)} 39 | [:li "item " e]])] ]))) 40 | 41 | (defn css-transition-example [] 42 | (let [state (r/atom true)] 43 | (fn [] 44 | [:div 45 | [:button 46 | {:on-click (fn [_] (swap! state not))} 47 | "Toggle"] 48 | [:> CSSTransition 49 | {:classNames "fade" 50 | :timeout 500 51 | :in @state 52 | :on-enter #(js/console.log "enter") 53 | :on-entering #(js/console.log "entering") 54 | :on-entered #(js/console.log "entered") 55 | :on-exit #(js/console.log "enter") 56 | :on-exiting #(js/console.log "exiting") 57 | :on-exited #(js/console.log "exited")} 58 | [:div {:class (if-not @state "hide")} "foobar"]]]))) 59 | 60 | (defn main [] 61 | [:div 62 | [:h1 "Transition group example"] 63 | [transition-group-example] 64 | 65 | [:h1 "CSS transition example"] 66 | [css-transition-example]]) 67 | 68 | (defn start [] 69 | (rdom/render [main] (js/document.getElementById "app"))) 70 | 71 | (start) 72 | -------------------------------------------------------------------------------- /examples/simple/README.md: -------------------------------------------------------------------------------- 1 | # Reagent example app 2 | 3 | `npx shadow-cljs watch app` 4 | -------------------------------------------------------------------------------- /examples/simple/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-context-example", 3 | "dependencies": { 4 | "react": "18.3.1", 5 | "react-dom": "18.3.1" 6 | }, 7 | "devDependencies": { 8 | "shadow-cljs": "2.28.20" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /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 |

Reagent example app – see README.md

11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/simple/shadow-cljs.edn: -------------------------------------------------------------------------------- 1 | {:source-paths ["src"] 2 | :dev-http {3000 ["resources/public" "target/public"]} 3 | :builds 4 | {:app {:target :browser 5 | :output-dir "target/public/js" 6 | :asset-path "/js" 7 | :devtools {:after-load simpleexample.core/run} 8 | :modules {:main {:entries [simpleexample.core] 9 | :init-fn simpleexample.core/run}}}} 10 | :dependencies [[reagent "1.2.0"]]} 11 | -------------------------------------------------------------------------------- /examples/simple/src/simpleexample/core.cljs: -------------------------------------------------------------------------------- 1 | (ns simpleexample.core 2 | (:require [reagent.core :as r] 3 | [reagent.dom :as rdom] 4 | [clojure.string :as str])) 5 | 6 | (defonce timer (r/atom (js/Date.))) 7 | 8 | (defonce time-color (r/atom "#f34")) 9 | 10 | (defonce time-updater (js/setInterval 11 | #(reset! timer (js/Date.)) 1000)) 12 | 13 | (defn greeting [message] 14 | [:h1 message]) 15 | 16 | (defn clock [] 17 | (let [time-str (-> @timer .toTimeString (str/split " ") first)] 18 | [:div.example-clock 19 | {:style {:color @time-color}} 20 | time-str])) 21 | 22 | (defn color-input [] 23 | [:div.color-input 24 | "Time color: " 25 | [:input {:type "text" 26 | :value @time-color 27 | :on-change #(reset! time-color (-> % .-target .-value))}]]) 28 | 29 | (defn simple-example [] 30 | [:div 31 | [greeting "Hello world, it is now"] 32 | [clock] 33 | [color-input]]) 34 | 35 | (defn ^:export run [] 36 | (rdom/render [simple-example] (js/document.getElementById "app"))) 37 | -------------------------------------------------------------------------------- /examples/todomvc/README.md: -------------------------------------------------------------------------------- 1 | # Reagent example app 2 | 3 | `npx shadow-cljs watch app` 4 | -------------------------------------------------------------------------------- /examples/todomvc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-context-example", 3 | "dependencies": { 4 | "react": "18.3.1", 5 | "react-dom": "18.3.1" 6 | }, 7 | "devDependencies": { 8 | "shadow-cljs": "2.28.20" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/todomvc/resources/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Example 6 | 7 | 8 | 9 |
10 |

Reagent example app – see README.md

11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/todomvc/shadow-cljs.edn: -------------------------------------------------------------------------------- 1 | {:source-paths ["src"] 2 | :dev-http {3000 ["resources/public" "target/public"]} 3 | :builds 4 | {:app {:target :browser 5 | :output-dir "target/public/js" 6 | :asset-path "/js" 7 | :devtools {:after-load todomvc.core/run} 8 | :modules {:main {:entries [todomvc.core] 9 | :init-fn todomvc.core/run}}}} 10 | :dependencies [[reagent "1.2.0"]]} 11 | -------------------------------------------------------------------------------- /examples/todomvc/src/todomvc/core.cljs: -------------------------------------------------------------------------------- 1 | (ns todomvc.core 2 | (:require [reagent.core :as r] 3 | [reagent.dom :as rdom] 4 | ["react" :as react] 5 | [clojure.string :as str])) 6 | 7 | (defonce todos (r/atom (sorted-map))) 8 | 9 | (defonce counter (r/atom 0)) 10 | 11 | (defn add-todo [text] 12 | (let [id (swap! counter inc)] 13 | (swap! todos assoc id {:id id :title text :done false}))) 14 | 15 | (defn toggle [id] (swap! todos update-in [id :done] not)) 16 | (defn save [id title] (swap! todos assoc-in [id :title] title)) 17 | (defn delete [id] (swap! todos dissoc id)) 18 | 19 | (defn mmap [m f a] (->> m (f a) (into (empty m)))) 20 | (defn complete-all [v] (swap! todos mmap map #(assoc-in % [1 :done] v))) 21 | (defn clear-done [] (swap! todos mmap remove #(get-in % [1 :done]))) 22 | 23 | (defonce init (do 24 | (add-todo "Rename Cloact to Reagent") 25 | (add-todo "Add undo demo") 26 | (add-todo "Make all rendering async") 27 | (add-todo "Allow any arguments to component functions") 28 | (complete-all true))) 29 | 30 | (defn todo-input [{:keys [title on-save on-stop input-ref]}] 31 | (let [val (r/atom title)] 32 | (fn [{:keys [id class placeholder]}] 33 | (let [stop (fn [_e] 34 | (reset! val "") 35 | (when on-stop (on-stop))) 36 | save (fn [e] 37 | (let [v (-> @val str str/trim)] 38 | (when-not (empty? v) 39 | (on-save v)) 40 | (stop e)))] 41 | [:input {:type "text" 42 | :value @val 43 | :ref input-ref 44 | :id id 45 | :class class 46 | :placeholder placeholder 47 | :on-blur save 48 | :on-change (fn [e] 49 | (reset! val (-> e .-target .-value))) 50 | :on-key-down (fn [e] 51 | (case (.-which e) 52 | 13 (save e) 53 | 27 (stop e) 54 | nil))}])))) 55 | 56 | (defn todo-edit [props] 57 | (let [ref (react/useRef)] 58 | (react/useEffect (fn [] 59 | (.focus (.-current ref)) 60 | js/undefined)) 61 | [todo-input (assoc props :input-ref ref)])) 62 | 63 | (defn todo-stats [{:keys [filt active done]}] 64 | (let [props-for (fn [name] 65 | {:class (when (= name @filt) "selected") 66 | :on-click #(reset! filt name)})] 67 | [:div 68 | [:span#todo-count 69 | [:strong active] " " (case active 1 "item" "items") " left"] 70 | [:ul#filters 71 | [:li [:a (props-for :all) "All"]] 72 | [:li [:a (props-for :active) "Active"]] 73 | [:li [:a (props-for :done) "Completed"]]] 74 | (when (pos? done) 75 | [:button#clear-completed {:on-click clear-done} 76 | "Clear completed " done])])) 77 | 78 | (defn todo-item [] 79 | (let [editing (r/atom false)] 80 | (fn [{:keys [id done title]}] 81 | [:li 82 | {:class [(when done "completed ") 83 | (when @editing "editing")]} 84 | [:div.view 85 | [:input.toggle 86 | {:type "checkbox" 87 | :checked done 88 | :on-change #(toggle id)}] 89 | [:label 90 | {:on-double-click #(reset! editing true)} 91 | title] 92 | [:button.destroy {:on-click #(delete id)}]] 93 | (when @editing 94 | [:f> todo-edit {:class "edit" 95 | :title title 96 | :on-save #(save id %) 97 | :on-stop #(reset! editing false)}])]))) 98 | 99 | (defn todo-app [] 100 | (let [filt (r/atom :all)] 101 | (fn [] 102 | (let [items (vals @todos) 103 | done (->> items (filter :done) count) 104 | active (- (count items) done)] 105 | [:div 106 | [:section#todoapp 107 | [:header#header 108 | [:h1 "todos"] 109 | [todo-input {:id "new-todo" 110 | :placeholder "What needs to be done?" 111 | :on-save add-todo}]] 112 | (when (-> items count pos?) 113 | [:div 114 | [:section#main 115 | [:input#toggle-all {:type "checkbox" :checked (zero? active) 116 | :on-change #(complete-all (pos? active))}] 117 | [:label {:for "toggle-all"} "Mark all as complete"] 118 | [:ul#todo-list 119 | (for [todo (filter (case @filt 120 | :active (complement :done) 121 | :done :done 122 | :all identity) items)] 123 | ^{:key (:id todo)} [todo-item todo])]] 124 | [:footer#footer 125 | [todo-stats {:active active :done done :filt filt}]]])] 126 | [:footer#info 127 | [:p "Double-click to edit a todo"]]])))) 128 | 129 | (defn ^:export run [] 130 | (rdom/render [todo-app] (js/document.getElementById "app"))) 131 | -------------------------------------------------------------------------------- /lib/modules.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | function exported_require (name) { 4 | switch (name) { 5 | case "react": return require("react"); 6 | case "react-dom": return require("react-dom"); 7 | case "react-dom/server": return require("react-dom/server"); 8 | default: 9 | console.error("Unknown module: ", name); 10 | } 11 | } 12 | 13 | if (!global.require) { 14 | global.require = exported_require; 15 | } 16 | -------------------------------------------------------------------------------- /logo/logo-text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reagent-project/reagent/1942c96c5eaf24c14b931a1256c2faed7cd37a0e/logo/logo-text.png -------------------------------------------------------------------------------- /logo/logo-text.svg: -------------------------------------------------------------------------------- 1 | 2 | 19 | 21 | 22 | 24 | image/svg+xml 25 | 27 | 28 | 29 | 30 | 31 | 33 | 55 | 60 | 67 | 68 | 73 | 78 | 83 | 88 | 93 | 98 | Reagent 109 | 110 | -------------------------------------------------------------------------------- /logo/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reagent-project/reagent/1942c96c5eaf24c14b931a1256c2faed7cd37a0e/logo/logo.png -------------------------------------------------------------------------------- /logo/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /outsite/public/README.md: -------------------------------------------------------------------------------- 1 | # Reagent documentation 2 | 3 | This repository contains documentation generated from the [Reagent](https://github.com/reagent-project/reagent) project. 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@reagent-project/reagent", 3 | "private": true, 4 | "dependencies": {}, 5 | "scripts": { 6 | "start": "lein figwheel client-npm" 7 | }, 8 | "devDependencies": { 9 | "@cljs-oss/module-deps": "1.1.1", 10 | "karma": "6.4.4", 11 | "karma-chrome-launcher": "3.2.0", 12 | "karma-cli": "2.0.0", 13 | "karma-cljs-test": "0.1.0", 14 | "karma-coverage": "2.2.1", 15 | "karma-junit-reporter": "2.0.1", 16 | "karma-sourcemap-loader": "0.4.0", 17 | "karma-webpack": "5.0.1", 18 | "md5-file": "5.0.0", 19 | "shadow-cljs": "2.28.20", 20 | "webpack": "5.96.1", 21 | "webpack-cli": "5.1.4", 22 | "prop-types": "15.8.1", 23 | "react": "18.3.1", 24 | "react-dom": "18.3.1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /prepare-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | npm install 4 | -------------------------------------------------------------------------------- /prerender/sitetools/prerender.cljs: -------------------------------------------------------------------------------- 1 | (ns sitetools.prerender 2 | (:require [reagentdemo.core :as demo] 3 | [clojure.string :as string] 4 | [goog.events :as evt] 5 | [reagent.core :as r] 6 | [reagent.dom.server :as server] 7 | [reagent.debug :refer [log]] 8 | [sitetools.core :as tools] 9 | 10 | ;; Node libs 11 | [path :as path] 12 | [md5-file :as md5-file] 13 | [path :as path] 14 | [fs :as fs])) 15 | 16 | (defn base [page] 17 | (let [depth (->> page tools/to-relative (re-seq #"/") count)] 18 | (->> "../" (repeat depth) (apply str)))) 19 | 20 | (defn danger [t s] 21 | [t {:dangerouslySetInnerHTML (r/unsafe-html s)}]) 22 | 23 | (defn add-cache-buster [resource-path path] 24 | (let [h (md5-file/sync (path/join resource-path path))] 25 | (str path "?" (subs h 0 6)))) 26 | 27 | (defn html-template [{:keys [title body-html page-conf 28 | js-file css-file 29 | js-resource-path site-dir]}] 30 | (server/render-to-static-markup 31 | [:html 32 | [:head 33 | [:meta {:charset 'utf-8}] 34 | [:meta {:name 'viewport 35 | :content "width=device-width, initial-scale=1.0"}] 36 | [:base {:href (-> page-conf :page-path base)}] 37 | [:link {:href (add-cache-buster site-dir css-file) 38 | :rel "stylesheet"}] 39 | [:title title]] 40 | [:body 41 | [:div 42 | {:id "main-content"} 43 | (danger :div body-html)] 44 | (danger :script (str "var pageConfig = " 45 | (-> page-conf clj->js js/JSON.stringify))) 46 | [:script {:src (add-cache-buster js-resource-path js-file) 47 | :type "text/javascript"}]]])) 48 | 49 | (defn gen-page [page-path conf] 50 | (tools/emit [:set-page page-path]) 51 | (let [conf (merge conf @tools/config) 52 | b (:body conf) 53 | bhtml (server/render-to-string b)] 54 | (str "\n" 55 | (html-template (assoc conf 56 | :page-conf {:page-path page-path} 57 | :body-html bhtml))))) 58 | 59 | (defn mkdirs [f] 60 | (doseq [d (reductions #(str %1 "/" %2) 61 | (-> (path/normalize f) 62 | (string/split #"/")))] 63 | (when-not (fs/existsSync d) 64 | (fs/mkdirSync d)))) 65 | 66 | (defn write-file [f content] 67 | (log "Write" f) 68 | (mkdirs (path/dirname f)) 69 | (fs/writeFileSync f content)) 70 | 71 | (defn write-resources [dir {:keys [css-file css-infiles]}] 72 | (write-file (path/join dir css-file) 73 | (->> css-infiles 74 | (map #(fs/readFileSync %)) 75 | (string/join "\n")))) 76 | 77 | (defn -main [& args] 78 | (log "Generating site") 79 | (demo/init!) 80 | (let [[js-resource-path] args 81 | {:keys [site-dir pages] :as conf} (assoc @tools/config :js-resource-path js-resource-path)] 82 | (write-resources site-dir conf) 83 | (doseq [f (keys pages)] 84 | (write-file (->> f tools/to-relative (path/join site-dir)) 85 | (gen-page f conf)))) 86 | (log "Wrote site") 87 | (js/process.exit 0)) 88 | 89 | (set! *main-cli-fn* -main) 90 | -------------------------------------------------------------------------------- /run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Kill all subshells with ctrl-c 4 | trap "kill 0" SIGINT 5 | 6 | reset='\033[0m' 7 | red='\033[0;31m' 8 | green='\033[0;32m' 9 | blue='\033[0;34m' 10 | 11 | EXIT=0 12 | 13 | SUMMARY="$blue##\n## SUMMARY\n##$reset\n\n" 14 | 15 | TOOL=$1 16 | 17 | for env in test-environments/*; do 18 | if [[ ! -f $env/test.sh ]]; then 19 | continue 20 | fi 21 | 22 | name=$(basename "$env") 23 | 24 | if [[ -n $TOOL ]]; then 25 | if [[ $name == bundle* ]]; then 26 | if [[ $TOOL != 'clj' ]]; then 27 | continue 28 | fi 29 | else 30 | if [[ $TOOL != 'lein' ]]; then 31 | continue 32 | fi 33 | fi 34 | fi 35 | 36 | echo -e "$blue##" 37 | echo -e "## TESTING $name" 38 | echo -e "##$reset" 39 | echo 40 | $env/test.sh 41 | if [[ $? != "0" ]]; then 42 | echo 43 | echo -e "${red}FAIL $name$reset" 44 | SUMMARY="$SUMMARY${red}FAIL $name$reset\n" 45 | EXIT=1 46 | else 47 | SUMMARY="$SUMMARY${green}OK $name$reset\n" 48 | fi 49 | echo 50 | echo 51 | done 52 | 53 | echo -e "$SUMMARY" 54 | 55 | exit $EXIT 56 | -------------------------------------------------------------------------------- /shadow-cljs.edn: -------------------------------------------------------------------------------- 1 | {:source-paths ["src" "test" "examples/todomvc/src" "examples/simple/src" "examples/geometry/src" "demo"] 2 | :dev-http {8090 {:roots ["site/public" "target/shadow-cljs/client/public" "classpath:public"]}} 3 | :builds 4 | {:client {:target :browser 5 | :output-dir "target/shadow-cljs/client/public/js" 6 | :asset-path "/js" 7 | :modules {:main {:entries [reagentdemo.dev]}} 8 | :devtools {:ignore-warnings true} 9 | :compiler-options {:warnings {:fn-deprecated false}}} 10 | :test {:target :karma 11 | :output-to "target/shadow-cljs/resources/public/js/karma.js" 12 | :output-dir "target/shadow-cljs/resources/public/js" 13 | :asset-path "js" 14 | :source-map true 15 | :compiler-options {:infer-externs :auto} 16 | :ns-regexp "(reagenttest\\.test.*|reagent\\..*-test)"}} 17 | :dependencies [[doo "0.1.11"] 18 | [funcool/promesa "11.0.678"]]} 19 | -------------------------------------------------------------------------------- /site/public/css/main.css: -------------------------------------------------------------------------------- 1 | 2 | * { 3 | -moz-box-sizing: border-box; 4 | -webkit-box-sizing: border-box; 5 | box-sizing: border-box; 6 | } 7 | 8 | 9 | .clearfix:before, .clearfix:after { 10 | content: " "; 11 | display: table; 12 | } 13 | 14 | .clearfix:after { 15 | clear: both; 16 | } 17 | 18 | body { 19 | background-color: #eeeeec; 20 | text-rendering: optimizeLegibility; 21 | } 22 | 23 | div.nav { 24 | position: absolute; 25 | top: 0; 26 | left: 0; 27 | right: 0; 28 | } 29 | 30 | ul.nav { 31 | margin: 0 auto; 32 | width: 750px; 33 | height: 40px; 34 | padding-left: 60px; 35 | padding-top: 18px; 36 | list-style-type: none; 37 | } 38 | 39 | ul.nav > li { 40 | float: left; 41 | margin-right: 60px; 42 | } 43 | 44 | ul.nav > li > a { 45 | text-decoration: none; 46 | text-transform: uppercase; 47 | letter-spacing: 4px; 48 | font-size: 11px; 49 | color: #777; 50 | } 51 | 52 | ul.nav > li.brand > a { 53 | color: #444; 54 | } 55 | 56 | @media (max-width: 768px) { 57 | div.nav { 58 | position: static; 59 | background-color: #fff; 60 | } 61 | ul.nav { 62 | width: 100%; 63 | padding: 18px 27px; 64 | } 65 | ul.nav > li { 66 | float: left; 67 | margin-right: 30px; 68 | } 69 | } 70 | 71 | .reagent-demo { 72 | background-color: #fff; 73 | margin: 60px -100px; 74 | padding: 29px 60px 45px 60px; 75 | } 76 | 77 | .reagent-demo > h1 { 78 | font-size: 48px; 79 | color: rgba(0, 0, 0, 0.75); 80 | font-family: 'HelveticaNeue-Light', 'Helvetica Neue', arial; 81 | font-weight: normal; 82 | line-height: 1.25em; 83 | margin-top: 0.25em; 84 | margin-bottom: 1em; 85 | } 86 | 87 | .reagent-demo > h1 > a { 88 | color: inherit; 89 | text-decoration: none; 90 | } 91 | 92 | @media (max-width: 768px) { 93 | body { 94 | width: auto; 95 | margin: 0 0; 96 | } 97 | .reagent-demo { 98 | margin: 0 0; 99 | padding: 36px 27px; 100 | width: 100%; 101 | } 102 | .github-badge { 103 | display: none; 104 | } 105 | .demo-source { 106 | overflow-x: scroll; 107 | } 108 | .test-output-mini { 109 | display: none; 110 | } 111 | } 112 | 113 | .test-output-mini { 114 | text-align: right; 115 | margin-right: -180px; 116 | } 117 | 118 | .reagent-demo h2 { 119 | font-size: 24px; 120 | line-height: 26px; 121 | margin-top: 38px; 122 | margin-bottom: 12px; 123 | color: rgba(0, 0, 0, 0.6); 124 | } 125 | 126 | .reagent-demo h2 + p { 127 | margin-top: 0; 128 | } 129 | 130 | .demo-text > p, .demo-text > ul { 131 | font-family: georgia, serif; 132 | font-size: 18px; 133 | line-height: 1.611em; 134 | color: rgba(0, 0, 0, 0.8); 135 | margin-bottom: 1em; 136 | martin-top: 1em; 137 | } 138 | 139 | .demo-text > p > code, .demo-text > ul > li > code { 140 | font-size: 16px; 141 | background-color: #f5f5f4; 142 | padding: 0 3px; 143 | } 144 | 145 | .demo-text > ul > li { 146 | margin-bottom: 0.5em; 147 | } 148 | 149 | .demo-example { 150 | background-color: #f6f6f5; 151 | } 152 | 153 | .demo-source { 154 | background-color: #fbfbfa; 155 | color: rgba(0, 0, 0, 0.75); 156 | } 157 | 158 | .demo-source pre { 159 | font-family: Consolas, Menlo, Courier, monospace; 160 | font-size: 14px; 161 | line-height: 20px; 162 | } 163 | 164 | .demo-example div, .demo-example p { 165 | color: rgba(0, 0, 0, 0.6); 166 | line-height: 1.2em; 167 | } 168 | 169 | .demo-example, .demo-source { 170 | margin: 29px -60px; 171 | padding: 29px 60px; 172 | } 173 | 174 | .demo-example + .demo-source { 175 | margin-top: -29px; 176 | } 177 | 178 | .simple-demo, .simple-demo input { 179 | font-size: 18px; 180 | } 181 | 182 | .demo-heading { 183 | margin-top: 0; 184 | color: #aaa; 185 | font-size: 14px; 186 | line-height: 29px; 187 | margin-top: 3px; 188 | } 189 | 190 | .demo-example-hide { 191 | margin-top: 3px; 192 | float: right; 193 | cursor: pointer; 194 | color: #bbb; 195 | } 196 | 197 | .news-read-more { 198 | text-decoration: none; 199 | font-size: 16px; 200 | } 201 | 202 | /* Color demo */ 203 | 204 | .color-plate { 205 | float: left; 206 | height: 100px; 207 | width: 100px; 208 | } 209 | 210 | .color-slider > input { 211 | width: 100%; 212 | } 213 | 214 | .color-samples { 215 | clear: both; 216 | padding-top: 0.5em; 217 | } 218 | 219 | /* Binary clock */ 220 | 221 | .clock-main { 222 | background: #333; 223 | color: #cdcdcd; 224 | padding-top: 55px; 225 | padding-left: 20px; 226 | float: left; 227 | font-size: 28px; 228 | line-height: 34px; 229 | width: 620px; 230 | cursor: pointer; 231 | } 232 | .clock-main.wide { 233 | width: 790px; 234 | } 235 | .clock-cell { 236 | width: 55px; 237 | height: 55px; 238 | text-align: center; 239 | margin: 0 20px 20px 0; 240 | } 241 | .clock-cell.dark { 242 | background-color: #454545; 243 | } 244 | .clock-cell.light { 245 | background-color: #eee; 246 | } 247 | .clock-col { 248 | margin: 0; 249 | float: left; 250 | } 251 | .clock-legend > .clock-cell { 252 | margin-top: 10px; 253 | } 254 | .clock-pair { 255 | margin: 0; 256 | float: left; 257 | } 258 | .clock-pair:not(:last-child) { 259 | margin-right: 20px; 260 | /* border-right: 1px solid #454545; */ 261 | } 262 | -------------------------------------------------------------------------------- /site/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 |

Run "lein figwheel", and open 11 | http://localhost:3449

12 |
13 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/clj-kondo/clj-kondo.exports/reagent/reagent/config.edn: -------------------------------------------------------------------------------- 1 | {:lint-as {reagent.core/with-let clojure.core/let}} 2 | -------------------------------------------------------------------------------- /src/reagent/core.clj: -------------------------------------------------------------------------------- 1 | (ns reagent.core 2 | (:require [reagent.ratom :as ra])) 3 | 4 | (defmacro with-let 5 | "Bind variables as with let, except that when used in a component 6 | the bindings are only evaluated once. Also takes an optional finally 7 | clause at the end, that is executed when the component is 8 | destroyed." 9 | [bindings & body] 10 | `(ra/with-let ~bindings ~@body)) 11 | 12 | (defmacro reaction 13 | "Creates Reaction from the body, returns a derefable 14 | containing the result of the body. If the body derefs 15 | reactive values (Reagent atoms, track, etc), the body 16 | will run again and the value of the Reaction is updated. 17 | 18 | New Reaction is created everytime reaction is called, 19 | so caller needs to take care that new reaction isn't created 20 | e.g. every component render, by using with-let, form-2 or form-3 21 | components or other solutions. Consider using reagent.core/track, 22 | for function that caches the derefable value, and can thus be safely 23 | used in e.g. render function safely." 24 | [& body] 25 | `(reagent.ratom/make-reaction 26 | (fn [] ~@body))) 27 | -------------------------------------------------------------------------------- /src/reagent/debug.clj: -------------------------------------------------------------------------------- 1 | (ns reagent.debug 2 | (:refer-clojure :exclude [prn println time]) 3 | (:require [cljs.analyzer :as analyzer])) 4 | 5 | (defmacro log 6 | "Print with console.log, if it exists." 7 | [& forms] 8 | `(when reagent.debug.has-console 9 | (.log js/console ~@forms))) 10 | 11 | (defmacro warn 12 | "Print with console.warn." 13 | [& forms] 14 | (when *assert* 15 | `(when reagent.debug.has-console 16 | (.warn (if reagent.debug.tracking 17 | reagent.debug.track-console 18 | js/console) 19 | (str "Warning: " ~@forms))))) 20 | 21 | (defmacro warn-unless 22 | [cond & forms] 23 | (when *assert* 24 | `(when (not ~cond) 25 | (warn ~@forms)))) 26 | 27 | (defmacro error 28 | "Print with console.error." 29 | [& forms] 30 | (when *assert* 31 | `(when reagent.debug.has-console 32 | (.error (if reagent.debug.tracking 33 | reagent.debug.track-console 34 | js/console) 35 | (str ~@forms))))) 36 | 37 | (defmacro println 38 | "Print string with console.log" 39 | [& forms] 40 | `(log (str ~@forms))) 41 | 42 | (defmacro prn 43 | "Like standard prn, but prints using console.log (so that we get 44 | nice clickable links to source in modern browsers)." 45 | [& forms] 46 | `(log (pr-str ~@forms))) 47 | 48 | (defmacro dbg 49 | "Useful debugging macro that prints the source and value of x, 50 | as well as package name and line number. Returns x." 51 | [x] 52 | (let [ns (str analyzer/*cljs-ns*)] 53 | `(let [x# ~x] 54 | (println (str "dbg " 55 | ~ns ":" 56 | ~(:line (meta &form)) 57 | ": " 58 | ~(pr-str x) 59 | ": " 60 | (pr-str x#))) 61 | x#))) 62 | 63 | (defmacro dev? 64 | "True if assertions are enabled." 65 | [] 66 | (if *assert* true false)) 67 | 68 | (defmacro time [& forms] 69 | (let [ns (str analyzer/*cljs-ns*) 70 | label (str ns ":" (:line (meta &form)))] 71 | `(let [label# ~label 72 | res# (do 73 | (js/console.time label#) 74 | ~@forms)] 75 | (js/console.timeEnd label#) 76 | res#))) 77 | 78 | (defmacro assert-some [value tag] 79 | `(assert ~value (str ~tag " must not be nil"))) 80 | 81 | (defmacro assert-component [value] 82 | `(assert (comp/reagent-component? ~value) 83 | (str "Expected a reagent component, not " 84 | (pr-str ~value)))) 85 | 86 | (defmacro assert-js-object [value] 87 | `(assert (not (map? ~value)) 88 | (str "Expected a JS object, not " 89 | (pr-str ~value)))) 90 | 91 | (defmacro assert-new-state [value] 92 | `(assert (or (nil? ~value) (map? ~value)) 93 | (str "Expected a valid new state, not " 94 | (pr-str ~value)))) 95 | 96 | (defmacro assert-callable [value] 97 | `(assert (ifn? ~value) 98 | (str "Expected something callable, not " 99 | (pr-str ~value)))) 100 | -------------------------------------------------------------------------------- /src/reagent/debug.cljs: -------------------------------------------------------------------------------- 1 | (ns reagent.debug 2 | (:require-macros [reagent.debug])) 3 | 4 | (def ^:const has-console (exists? js/console)) 5 | 6 | (def ^boolean tracking false) 7 | 8 | (defonce warnings (atom nil)) 9 | 10 | (defonce track-console 11 | (let [o #js {}] 12 | (set! (.-warn o) 13 | (fn [& args] 14 | (swap! warnings update-in [:warn] conj (apply str args)))) 15 | (set! (.-error o) 16 | (fn [& args] 17 | (swap! warnings update-in [:error] conj (apply str args)))) 18 | o)) 19 | 20 | (defn track-warnings [f] 21 | (set! tracking true) 22 | (reset! warnings nil) 23 | (f) 24 | (let [warns @warnings] 25 | (reset! warnings nil) 26 | (set! tracking false) 27 | warns)) 28 | -------------------------------------------------------------------------------- /src/reagent/dom.cljs: -------------------------------------------------------------------------------- 1 | (ns reagent.dom 2 | (:require [react-dom :as react-dom] 3 | [reagent.impl.util :as util] 4 | [reagent.impl.template :as tmpl] 5 | [reagent.impl.batching :as batch] 6 | [reagent.impl.protocols :as p] 7 | [reagent.ratom :as ratom])) 8 | 9 | (defonce ^:private roots (atom {})) 10 | 11 | (defn- unmount-comp [container] 12 | (swap! roots dissoc container) 13 | (react-dom/unmountComponentAtNode container)) 14 | 15 | (defn- render-comp [comp container callback] 16 | (binding [util/*always-update* true] 17 | (react-dom/render (comp) container 18 | (fn [] 19 | (binding [util/*always-update* false] 20 | (swap! roots assoc container comp) 21 | (batch/flush-after-render) 22 | (if (some? callback) 23 | (callback))))))) 24 | 25 | (defn- re-render-component [comp container] 26 | (render-comp comp container nil)) 27 | 28 | (defn render 29 | "Render a Reagent component into the DOM. The first argument may be 30 | either a vector (using Reagent's Hiccup syntax), or a React element. 31 | The second argument should be a DOM node. 32 | 33 | Optionally takes a callback that is called when the component is in place. 34 | 35 | Returns the mounted component instance." 36 | ([comp container] 37 | (render comp container tmpl/*current-default-compiler*)) 38 | ([comp container callback-or-compiler] 39 | (ratom/flush!) 40 | (let [[compiler callback] (cond 41 | (map? callback-or-compiler) 42 | [(:compiler callback-or-compiler) (:callback callback-or-compiler)] 43 | 44 | (fn? callback-or-compiler) 45 | [tmpl/*current-default-compiler* callback-or-compiler] 46 | 47 | :else 48 | [callback-or-compiler nil]) 49 | f (fn [] 50 | (p/as-element compiler (if (fn? comp) (comp) comp)))] 51 | (render-comp f container callback)))) 52 | 53 | (defn unmount-component-at-node 54 | "Remove a component from the given DOM node." 55 | [container] 56 | (unmount-comp container)) 57 | 58 | (defn dom-node 59 | "Returns the root DOM node of a mounted component." 60 | {:deprecated "1.2.0"} 61 | [this] 62 | (react-dom/findDOMNode this)) 63 | 64 | (defn force-update-all 65 | "Force re-rendering of all mounted Reagent components. This is 66 | probably only useful in a development environment, when you want to 67 | update components in response to some dynamic changes to code. 68 | 69 | Note that force-update-all may not update root components. This 70 | happens if a component 'foo' is mounted with `(render [foo])` (since 71 | functions are passed by value, and not by reference, in 72 | ClojureScript). To get around this you'll have to introduce a layer 73 | of indirection, for example by using `(render [#'foo])` instead." 74 | {:deprecated "1.2.0"} 75 | [] 76 | (ratom/flush!) 77 | (doseq [[container comp] @roots] 78 | (re-render-component comp container)) 79 | (batch/flush-after-render)) 80 | -------------------------------------------------------------------------------- /src/reagent/dom/client.cljs: -------------------------------------------------------------------------------- 1 | (ns reagent.dom.client 2 | (:require ["react" :as react] 3 | ["react-dom/client" :as react-dom-client] 4 | [reagent.impl.batching :as batch] 5 | [reagent.impl.protocols :as p] 6 | [reagent.impl.template :as tmpl] 7 | [reagent.impl.util :as util] 8 | [goog.object :as gobj])) 9 | 10 | (defn create-root 11 | "Create a React Root connected to given container DOM element." 12 | [container] 13 | (react-dom-client/createRoot container)) 14 | 15 | (defn unmount 16 | "Unmount the given React Root" 17 | [root] 18 | (.unmount root)) 19 | 20 | (defn- reagent-root [^js js-props] 21 | ;; This will flush initial r/after-render callbacks. 22 | ;; Later that queue will be flushed on Reagent render-loop. 23 | (let [el (gobj/get js-props "comp")] 24 | (react/useEffect (fn [] 25 | (binding [util/*always-update* false] 26 | (batch/flush-after-render) 27 | js/undefined))) 28 | (binding [util/*always-update* true] 29 | (el)))) 30 | 31 | (defn render 32 | "Render the given Reagent element (i.e. Hiccup data) 33 | into a given React root." 34 | ([root el] 35 | (render root el tmpl/*current-default-compiler*)) 36 | ([root el compiler] 37 | (let [;; Not sure if this should be fn here? 38 | ;; At least this moves the as-element call to the reagent-root 39 | ;; render, and handles the *always-update* binding correctly? 40 | comp (fn [] (p/as-element compiler el))] 41 | (.render root (react/createElement reagent-root #js {:comp comp}))))) 42 | 43 | (defn hydrate-root 44 | ([container el] 45 | (hydrate-root container el nil)) 46 | ([container el {:keys [compiler on-recoverable-error identifier-prefix] 47 | :or {compiler tmpl/*current-default-compiler*}}] 48 | (let [comp (fn [] (p/as-element compiler el))] 49 | (react-dom-client/hydrateRoot container (react/createElement reagent-root #js {:comp comp}))))) 50 | -------------------------------------------------------------------------------- /src/reagent/dom/server.cljs: -------------------------------------------------------------------------------- 1 | (ns reagent.dom.server 2 | (:require ["react-dom/server" :as dom-server] 3 | [reagent.impl.util :as util] 4 | [reagent.impl.template :as tmpl] 5 | [reagent.impl.protocols :as p] 6 | [reagent.ratom :as ratom])) 7 | 8 | (defn render-to-string 9 | "Turns a component into an HTML string." 10 | ([component] 11 | (render-to-string component tmpl/*current-default-compiler*)) 12 | ([component compiler] 13 | (ratom/flush!) 14 | (binding [util/*non-reactive* true] 15 | (dom-server/renderToString (p/as-element compiler component))))) 16 | 17 | (defn render-to-static-markup 18 | "Turns a component into an HTML string, without data-react-id attributes, etc." 19 | ([component] 20 | (render-to-static-markup component tmpl/*current-default-compiler*)) 21 | ([component compiler] 22 | (ratom/flush!) 23 | (binding [util/*non-reactive* true] 24 | (dom-server/renderToStaticMarkup (p/as-element compiler component))))) 25 | -------------------------------------------------------------------------------- /src/reagent/impl/batching.cljs: -------------------------------------------------------------------------------- 1 | (ns reagent.impl.batching 2 | (:refer-clojure :exclude [flush]) 3 | (:require [reagent.debug :refer-macros [assert-some]] 4 | [reagent.impl.util :refer [is-client]])) 5 | 6 | ;;; Update batching 7 | 8 | (defonce mount-count 0) 9 | 10 | (defn next-mount-count [] 11 | (set! mount-count (inc mount-count))) 12 | 13 | (defn fake-raf [f] 14 | (js/setTimeout f 16)) 15 | 16 | (def next-tick 17 | (if-not is-client 18 | fake-raf 19 | (let [w js/window] 20 | (.bind (or (.-requestAnimationFrame w) 21 | (.-webkitRequestAnimationFrame w) 22 | (.-mozRequestAnimationFrame w) 23 | (.-msRequestAnimationFrame w) 24 | fake-raf) 25 | w)))) 26 | 27 | (defn compare-mount-order 28 | [^clj c1 ^clj c2] 29 | (- (.-cljsMountOrder c1) 30 | (.-cljsMountOrder c2))) 31 | 32 | (defn run-queue [a] 33 | ;; sort components by mount order, to make sure parents 34 | ;; are rendered before children 35 | (.sort a compare-mount-order) 36 | (dotimes [i (alength a)] 37 | (let [^js/React.Component c (aget a i)] 38 | (when (true? (.-cljsIsDirty c)) 39 | (.forceUpdate c))))) 40 | 41 | 42 | ;; Set from ratom.cljs 43 | (defonce ratom-flush (fn [])) 44 | 45 | (defn run-funs [fs] 46 | (dotimes [i (alength fs)] 47 | ((aget fs i)))) 48 | 49 | (defn enqueue [^clj queue fs f] 50 | (assert-some f "Enqueued function") 51 | (.push fs f) 52 | (.schedule queue)) 53 | 54 | (deftype RenderQueue [^:mutable ^boolean scheduled?] 55 | Object 56 | (schedule [this] 57 | (when-not scheduled? 58 | (set! scheduled? true) 59 | (next-tick #(.run-queues this)))) 60 | 61 | (queue-render [this c] 62 | (when (nil? (.-componentQueue this)) 63 | (set! (.-componentQueue this) #js [])) 64 | (enqueue this (.-componentQueue this) c)) 65 | 66 | (add-before-flush [this f] 67 | (when (nil? (.-beforeFlush this)) 68 | (set! (.-beforeFlush this) #js [])) 69 | (enqueue this (.-beforeFlush this) f)) 70 | 71 | (add-after-render [this f] 72 | (when (nil? (.-afterRender this)) 73 | (set! (.-afterRender this) #js [])) 74 | (enqueue this (.-afterRender this) f)) 75 | 76 | (run-queues [this] 77 | (set! scheduled? false) 78 | (.flush-queues this)) 79 | 80 | (flush-before-flush [this] 81 | (when-some [fs (.-beforeFlush this)] 82 | (set! (.-beforeFlush this) nil) 83 | (run-funs fs))) 84 | 85 | (flush-render [this] 86 | (when-some [fs (.-componentQueue this)] 87 | (set! (.-componentQueue this) nil) 88 | (run-queue fs))) 89 | 90 | (flush-after-render [this] 91 | (when-some [fs (.-afterRender this)] 92 | (set! (.-afterRender this) nil) 93 | (run-funs fs))) 94 | 95 | (flush-queues [this] 96 | (.flush-before-flush this) 97 | (ratom-flush) 98 | (.flush-render this) 99 | (.flush-after-render this))) 100 | 101 | (def render-queue (->RenderQueue false)) 102 | 103 | (defn flush [] 104 | (.flush-queues render-queue)) 105 | 106 | (defn flush-after-render [] 107 | (.flush-after-render render-queue)) 108 | 109 | (defn queue-render [^clj c] 110 | (when-not (.-cljsIsDirty c) 111 | (set! (.-cljsIsDirty c) true) 112 | (.queue-render render-queue c))) 113 | 114 | (defn mark-rendered [^clj c] 115 | (set! (.-cljsIsDirty c) false)) 116 | 117 | (defn do-before-flush [f] 118 | (.add-before-flush render-queue f)) 119 | 120 | (defn do-after-render [f] 121 | (.add-after-render render-queue f)) 122 | 123 | (defn schedule [] 124 | (when (false? (.-scheduled? render-queue)) 125 | (.schedule render-queue))) 126 | -------------------------------------------------------------------------------- /src/reagent/impl/protocols.cljs: -------------------------------------------------------------------------------- 1 | (ns reagent.impl.protocols) 2 | 3 | (defprotocol Compiler 4 | (get-id [this]) 5 | (parse-tag [this tag-name tag-value]) 6 | (as-element [this x]) 7 | (make-element [this argv component jsprops first-child])) 8 | 9 | -------------------------------------------------------------------------------- /src/reagent/interop.clj: -------------------------------------------------------------------------------- 1 | (ns reagent.interop) 2 | 3 | ; taken from cljs.core 4 | ; https://github.com/binaryage/cljs-oops/issues/14 5 | (defmacro unchecked-aget 6 | ([array idx] 7 | (list 'js* "(~{}[~{}])" array idx)) 8 | ([array idx & idxs] 9 | (let [astr (apply str (repeat (count idxs) "[~{}]"))] 10 | `(~'js* ~(str "(~{}[~{}]" astr ")") ~array ~idx ~@idxs)))) 11 | 12 | ; taken from cljs.core 13 | ; https://github.com/binaryage/cljs-oops/issues/14 14 | (defmacro unchecked-aset 15 | ([array idx val] 16 | (list 'js* "(~{}[~{}] = ~{})" array idx val)) 17 | ([array idx idx2 & idxv] 18 | (let [n (dec (count idxv)) 19 | astr (apply str (repeat n "[~{}]"))] 20 | `(~'js* ~(str "(~{}[~{}][~{}]" astr " = ~{})") ~array ~idx ~idx2 ~@idxv)))) 21 | -------------------------------------------------------------------------------- /src/reagent/interop.cljs: -------------------------------------------------------------------------------- 1 | (ns reagent.interop 2 | (:require-macros [reagent.interop])) 3 | -------------------------------------------------------------------------------- /src/reagent/ratom.clj: -------------------------------------------------------------------------------- 1 | (ns reagent.ratom 2 | (:refer-clojure :exclude [run!]) 3 | (:require [reagent.debug :as d] 4 | [reagent.interop :as interop])) 5 | 6 | ;; Note: this macro is duplicated in reagent.core, 7 | ;; with a docstring. 8 | (defmacro reaction 9 | "Prefer using reagent.core/reaction." 10 | [& body] 11 | `(reagent.ratom/make-reaction 12 | (fn [] ~@body))) 13 | 14 | (defmacro run! 15 | "Creates a Reaction from the body, and runs the body immediately 16 | and again whenever atoms deferenced in the body 17 | change. Body should side effect. 18 | 19 | Use dispose! to stop running the Reaction." 20 | [& body] 21 | `(let [co# (reagent.ratom/make-reaction (fn [] ~@body) 22 | :auto-run true)] 23 | (deref co#) 24 | co#)) 25 | 26 | (defmacro with-let [bindings & body] 27 | (assert (vector? bindings) 28 | (str "with-let bindings must be a vector, not " 29 | (pr-str bindings))) 30 | (let [v (with-meta (gensym "with-let") {:tag 'clj}) 31 | k (keyword v) 32 | init (gensym "init") 33 | ;; V is a reaction, which holds a JS array. 34 | ;; If the array is empty, initialize values and store to the 35 | ;; array, using binding index % 2 to access the array. 36 | ;; After init, the bindings are just bound to the values in the array. 37 | bs (into [init `(zero? (alength ~v))] 38 | (map-indexed (fn [i x] 39 | (if (even? i) 40 | x 41 | (let [j (quot i 2)] 42 | ;; Issue 525 43 | ;; If binding value is not yet set, 44 | ;; try setting it again. This should 45 | ;; also throw errors for each render 46 | ;; and prevent the body being called 47 | ;; if bindings throw errors. 48 | `(if (or ~init 49 | (not (.hasOwnProperty ~v ~j))) 50 | (interop/unchecked-aset ~v ~j ~x) 51 | (interop/unchecked-aget ~v ~j))))) 52 | bindings)) 53 | [forms destroy] (let [fin (last body)] 54 | (if (and (list? fin) 55 | (= 'finally (first fin))) 56 | [(butlast body) `(fn [] ~@(rest fin))] 57 | [body nil])) 58 | add-destroy (when destroy 59 | (list 60 | `(let [destroy# ~destroy] 61 | (if (reagent.ratom/reactive?) 62 | (when (nil? (.-destroy ~v)) 63 | (set! (.-destroy ~v) destroy#)) 64 | (destroy#))))) 65 | asserting (if *assert* true false) 66 | res (gensym "res")] 67 | `(let [~v (reagent.ratom/with-let-values ~k)] 68 | ~(when asserting 69 | `(when-some [^clj c# reagent.ratom/*ratom-context*] 70 | (when (== (.-generation ~v) (.-ratomGeneration c#)) 71 | (d/error "Warning: The same with-let is being used more " 72 | "than once in the same reactive context.")) 73 | (set! (.-generation ~v) (.-ratomGeneration c#)))) 74 | (let ~(into bs [res `(do ~@forms)]) 75 | ~@add-destroy 76 | ~res)))) 77 | -------------------------------------------------------------------------------- /test-environments/browser-cljsjs-prod/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ex 3 | rm -rf target/cljsbuild/prod-test/ 4 | lein doo chrome-headless prod-test once 5 | test -f target/cljsbuild/prod-test/main.js 6 | 7 | gzip -fk target/cljsbuild/prod-test/main.js 8 | ls -lh target/cljsbuild/prod-test/ 9 | -------------------------------------------------------------------------------- /test-environments/browser-cljsjs/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ex 3 | rm -rf target/cljsbuild/test/ 4 | COVERAGE=1 lein doo chrome-headless test once 5 | test -f target/cljsbuild/test/out/cljsjs/react/development/react.inc.js 6 | -------------------------------------------------------------------------------- /test-environments/browser-npm-prod/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ex 4 | 5 | rm -rf target/cljsbuild/prod-test-npm/ 6 | lein doo chrome-headless prod-test-npm once 7 | test -f target/cljsbuild/prod-test-npm/main.js 8 | 9 | gzip -fk target/cljsbuild/prod-test-npm/main.js 10 | ls -lh target/cljsbuild/prod-test-npm/ 11 | -------------------------------------------------------------------------------- /test-environments/browser-npm/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ex 3 | rm -rf target/cljsbuild/test-npm/ 4 | lein doo chrome-headless test-npm once 5 | test -f target/cljsbuild/test-npm/out/node_modules/react/index.js 6 | -------------------------------------------------------------------------------- /test-environments/bundle-adv/README.md: -------------------------------------------------------------------------------- 1 | # Advanced optimized JS bundle 2 | 3 | - `tesh.sh` 4 | - Creates `karma.js` bundle using `karma.edn`, including the test suite. 5 | - Runs Karma with `karma.config.js`. 6 | - `build.sh` 7 | - Creates `index.js` bundle which contains the demo site, without test suite. 8 | - Runs Webpack and copies static files to target folder. 9 | - The bundles share the ClojureScript compiler output-dir, so need to 10 | compile everything twice. 11 | -------------------------------------------------------------------------------- /test-environments/bundle-adv/build.edn: -------------------------------------------------------------------------------- 1 | {:main reagentdemo.prod 2 | :optimizations :advanced 3 | :output-to "target/bundle-adv/resources/public/js/out/index.js" 4 | :output-dir "target/bundle-adv/resources/public/js/out" 5 | ; :pseudo-names true 6 | :elide-asserts true 7 | :target :bundle 8 | :closure-defines {cljs.core/*global* "window"}} 9 | -------------------------------------------------------------------------------- /test-environments/bundle-adv/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ex 3 | clj -M -m cljs.main -co test-environments/bundle-adv/build.edn -v --compile 4 | npx webpack --config=test-environments/bundle-adv/webpack.config.js 5 | cp -r site/public/index.html site/public/css target/bundle-adv/resources/public/ 6 | -------------------------------------------------------------------------------- /test-environments/bundle-adv/karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | config.set({ 3 | browsers: ['ChromeHeadless'], 4 | basePath: '../../target/bundle-adv/resources/public/', 5 | files: ['js/out/karma.js'], 6 | frameworks: ['cljs-test'], 7 | preprocessors: { 8 | 'js/out/karma.js': ['webpack', 'sourcemap'] 9 | }, 10 | colors: true, 11 | logLevel: config.LOG_INFO, 12 | client: { 13 | args: ['reagenttest.runtests.karma_tests'], 14 | }, 15 | singleRun: true, 16 | webpack: { 17 | mode: 'production' 18 | } 19 | }); 20 | }; 21 | -------------------------------------------------------------------------------- /test-environments/bundle-adv/karma.edn: -------------------------------------------------------------------------------- 1 | {:main reagenttest.runtests 2 | :optimizations :advanced 3 | :output-to "target/bundle-adv/resources/public/js/out/karma.js" 4 | :output-dir "target/bundle-adv/resources/public/js/out" 5 | ; :pseudo-names true 6 | :elide-asserts true 7 | :target :bundle 8 | :closure-defines {cljs.core/*global* "window"}} 9 | -------------------------------------------------------------------------------- /test-environments/bundle-adv/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ex 3 | clojure -M -m cljs.main -co test-environments/bundle-adv/karma.edn -v --compile 4 | npx karma start test-environments/bundle-adv/karma.conf.js 5 | 6 | gzip -fk target/bundle-adv/resources/public/js/out/karma.js 7 | ls -lh target/bundle-adv/resources/public/js/out/ 8 | -------------------------------------------------------------------------------- /test-environments/bundle-adv/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mode: 'production', 3 | entry: './target/bundle-adv/resources/public/js/out/index.js', 4 | output: { 5 | path: __dirname + "/../../target/bundle-adv/resources/public/js/", 6 | filename: 'main.js' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /test-environments/bundle/build.edn: -------------------------------------------------------------------------------- 1 | {:main reagenttest.runtests 2 | :output-to "target/bundle/resources/public/js/out/index.js" 3 | :output-dir "target/bundle/resources/public/js/out" 4 | :asset-path "js/out" 5 | :target :bundle 6 | ; :bundle-cmd {:none ["npx" "webpack" "--mode=development"] 7 | ; :default ["npx" "webpack"]} 8 | } 9 | -------------------------------------------------------------------------------- /test-environments/bundle/karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | config.set({ 3 | browsers: ['ChromeHeadless'], 4 | basePath: '../../target/bundle/resources/public/', 5 | files: [ 6 | // Karma will try running test adapter before all 7 | // files loaded by Cljs are ready. 8 | '../../../../test-environments/bundle/workaround.js', 9 | 'js/out/index.js', 10 | {pattern: 'js/out/**/*.js', included: false}, 11 | // Source maps 12 | {pattern: 'js/out/**/*.cljs', included: false}, 13 | {pattern: 'js/out/**/*.js.map', included: false} 14 | ], 15 | frameworks: ['cljs-test'], 16 | preprocessors: { 17 | 'js/out/index.js': ['webpack', 'sourcemap'] 18 | }, 19 | // Cljs asset-path 20 | proxies: { 21 | '/js/out/': '/base/js/out/' 22 | }, 23 | colors: true, 24 | logLevel: config.LOG_INFO, 25 | client: { 26 | args: ['reagenttest.runtests.karma_tests'], 27 | }, 28 | singleRun: true, 29 | webpack: { 30 | mode: 'development' 31 | } 32 | }); 33 | }; 34 | -------------------------------------------------------------------------------- /test-environments/bundle/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ex 3 | clojure -M -m cljs.main -co test-environments/bundle/build.edn --compile 4 | npx karma start test-environments/bundle/karma.conf.js 5 | -------------------------------------------------------------------------------- /test-environments/bundle/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mode: 'development', 3 | entry: './target/bundle/resources/public/js/out/index.js', 4 | output: { 5 | path: __dirname + "/target/bundle/resources/public/js/", 6 | filename: 'main.js' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /test-environments/bundle/workaround.js: -------------------------------------------------------------------------------- 1 | window.__karma__.loaded_real = window.__karma__.loaded; 2 | window.__karma__.loaded = function() { 3 | 4 | }; 5 | -------------------------------------------------------------------------------- /test-environments/node-cljsjs/test_disabled.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ex 3 | rm -rf target/cljsbuild/node-test/ 4 | lein doo node node-test once 5 | test -f target/cljsbuild/node-test/out/cljsjs/react/development/react.inc.js 6 | -------------------------------------------------------------------------------- /test-environments/node-npm/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ex 3 | rm -rf target/cljsbuild/node-test-npm/ 4 | lein doo node node-test-npm once 5 | test ! -f target/cljsbuild/node-test-npm/out/node_modules/react/index.js 6 | grep "reagent.impl.template.node\$module\$react = require('react')" target/cljsbuild/node-test-npm/out/reagent/impl/template.js 7 | -------------------------------------------------------------------------------- /test-environments/shadow-cljs-prod/karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | config.set({ 3 | browsers: ['ChromeHeadless'], 4 | basePath: '../../target/shadow-cljs/resources/public/js/', 5 | files: ['karma.js'], 6 | frameworks: ['cljs-test'], 7 | plugins: ['karma-cljs-test', 'karma-chrome-launcher'], 8 | colors: true, 9 | logLevel: config.LOG_INFO, 10 | client: { 11 | args: ['shadow.test.karma.init'], 12 | singleRun: true 13 | } 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /test-environments/shadow-cljs-prod/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ex 3 | rm -rf target/shadow-cljs/ 4 | npx shadow-cljs release test 5 | test -f target/shadow-cljs/resources/public/js/karma.js 6 | npx karma start test-environments/shadow-cljs-prod/karma.conf.js --single-run 7 | -------------------------------------------------------------------------------- /test/reagent/impl/template_test.cljs: -------------------------------------------------------------------------------- 1 | (ns reagent.impl.template-test 2 | (:require [clojure.test :as t :refer [deftest is testing]] 3 | [reagent.impl.template :as tmpl] 4 | [goog.object :as gobj])) 5 | 6 | (deftest cached-prop-name 7 | (is (= "className" 8 | (tmpl/cached-prop-name :class)))) 9 | 10 | (deftest cached-custom-prop-name 11 | (is (= "class" 12 | (tmpl/cached-custom-prop-name :class)))) 13 | 14 | ;; Cljs.test prints better error if check is Cljs function 15 | (defn js-equal? [a b] 16 | (gobj/equals a b)) 17 | 18 | (deftest convert-props-test 19 | (is (js-equal? #js {:className "a"} 20 | (tmpl/convert-props {:class "a"} (tmpl/HiccupTag. nil nil nil false)))) 21 | (is (js-equal? #js {:class "a"} 22 | (tmpl/convert-props {:class "a"} (tmpl/HiccupTag. nil nil nil true)))) 23 | (is (js-equal? #js {:className "a b" :id "a"} 24 | (tmpl/convert-props {:class "b"} (tmpl/HiccupTag. nil "a" "a" false))))) 25 | -------------------------------------------------------------------------------- /test/reagent/impl/util_test.cljs: -------------------------------------------------------------------------------- 1 | (ns reagent.impl.util-test 2 | (:require [clojure.test :refer [deftest is testing]] 3 | [reagent.debug :refer [dev?]] 4 | [reagent.core :as r] 5 | [reagent.impl.util :as util])) 6 | 7 | (deftest class-names-test 8 | (is (= nil 9 | (r/class-names) 10 | (r/class-names nil) 11 | (r/class-names []) 12 | (r/class-names nil []))) 13 | (is (= "a b" 14 | (r/class-names ["a" "b"]) 15 | (r/class-names "a" "b"))) 16 | (is (= "a" 17 | (r/class-names :a) 18 | (r/class-names [:a]) 19 | (r/class-names nil "a") 20 | (r/class-names [] nil "a"))) 21 | (is (= "a b c d" 22 | (r/class-names "a" "b" nil ["c" "d"])))) 23 | 24 | (deftest dash-to-prop-name-test 25 | (is (= "tabIndex" (util/dash-to-prop-name :tab-index))) 26 | (is (= "data-foo-bar" (util/dash-to-prop-name :data-foo-bar))) 27 | (is (= "string-Value" (util/dash-to-prop-name "string-Value"))) 28 | (is (= "aBC" (util/dash-to-prop-name :a-b-c)))) 29 | 30 | (deftest dash-to-method-name-test 31 | (is (= "string-Value" 32 | (util/dash-to-method-name "string-Value"))) 33 | (is (= "componentDidMount" 34 | (util/dash-to-method-name :component-did-mount))) 35 | (is (= "componentDidMount" 36 | (util/dash-to-method-name :componentDidMount))) 37 | (is (= "UNSAFE_componentDidMount" 38 | (util/dash-to-method-name :unsafe-component-did-mount))) 39 | (is (= "UNSAFE_componentDidMount" 40 | (util/dash-to-method-name :unsafe_componentDidMount))) 41 | (is (= "aBC" 42 | (util/dash-to-method-name :a-b-c)))) 43 | 44 | ; (simple-benchmark [] 45 | ; (do (util/class-names "a" "b") 46 | ; (util/class-names nil "a") 47 | ; (util/class-names "a" nil)) 48 | ; 10000) 49 | 50 | ; (simple-benchmark [] 51 | ; (util/class-names "a" "b" nil "c" "d") 52 | ; 10000) 53 | 54 | (deftest merge-props-test 55 | (testing "no arguments" 56 | (is (nil? (r/merge-props)))) 57 | 58 | (testing "one argument" 59 | (is (nil? (r/merge-props nil))) 60 | (is (= {:foo :bar} (r/merge-props {:foo :bar})))) 61 | 62 | (testing "two arguments" 63 | (is (= {:disabled false :style {:flex 1 :flex-direction "row"} :class "foo bar"} 64 | (r/merge-props {:disabled true :style {:flex 1} :class "foo"} 65 | {:disabled false :style {:flex-direction "row"} :class "bar"}))) 66 | 67 | (is (= {:disabled true} 68 | (r/merge-props nil {:disabled true}) 69 | (r/merge-props {:disabled true} nil) ))) 70 | 71 | (testing "two arguments without classes" 72 | (is (= {:disabled false :style {:flex 1 :flex-direction "row"}} 73 | (r/merge-props {:disabled true :style {:flex 1}} 74 | {:disabled false :style {:flex-direction "row"}})))) 75 | 76 | (testing "n arguments" 77 | (is (= {:disabled false 78 | :checked true 79 | :style {:align-items "flex-end" 80 | :justify-content "center"} 81 | :class "foo bar baz quux"} 82 | (r/merge-props {:disabled false 83 | :checked false 84 | :style {:align-items "flex-end"} 85 | :class "foo"} 86 | {:disabled false 87 | :checked false 88 | :class "bar"} 89 | {:disabled true 90 | :style {:justify-content "center"} 91 | :class "baz"} 92 | {:disabled false 93 | :checked true 94 | :class "quux"} 95 | nil)))) 96 | 97 | (testing ":class" 98 | (is (= {:class "foo bar baz quux"} 99 | (r/merge-props {:class "foo bar"} 100 | {:class ["baz" "quux"]}) 101 | (r/merge-props nil {:class ["foo" "bar" "baz" "quux"]}) 102 | (r/merge-props {:class ["foo" "bar" "baz" "quux"]} nil) 103 | (r/merge-props {:class ["foo" "bar" "baz" "quux"]}) 104 | (r/merge-props {:class "foo bar"} {:class ["baz"]} {:class ["quux"]})))) 105 | 106 | (when (dev?) 107 | (testing "assertion" 108 | (is (thrown-with-msg? js/Error #"Assert failed: Property must be a map, not" (r/merge-props #js {} {:class "foo"})))))) 109 | 110 | (deftest partial-fn-test 111 | (is (= (util/make-partial-fn println ["a"]) 112 | (util/make-partial-fn println ["a"]))) 113 | 114 | (is (= (hash (util/make-partial-fn println ["a"])) 115 | (hash (util/make-partial-fn println ["a"])))) 116 | 117 | (testing "partial fn invoke" 118 | ;; Test all IFn arities 119 | (doseq [c (range 0 23)] 120 | (is (= (seq (repeat c "a")) ((util/make-partial-fn (fn [& args] args) (repeat c "a"))))))) 121 | 122 | (is (not (= (util/make-partial-fn println ["a"]) 123 | nil)))) 124 | 125 | (deftest fun-name-test 126 | (when (dev?) 127 | (is (= "reagent.impl.util_test.foobar" 128 | (util/fun-name (fn foobar [] 1))))) 129 | 130 | (is (= "foobar" 131 | (let [f (fn [] 1)] 132 | (set! (.-displayName f) "foobar") 133 | (util/fun-name f)))) ) 134 | 135 | (defn foo [m] 136 | [:h1]) 137 | 138 | (deftest react-key-from-vec-test 139 | (is (= 1 (util/react-key-from-vec ^{:key 1} [:foo "bar"]))) 140 | (is (= 1 (util/react-key-from-vec [:foo {:key 1} "bar"]))) 141 | (is (= 1 (util/react-key-from-vec [:> "div" {:key 1} "bar"]))) 142 | (is (= 1 (util/react-key-from-vec [:f> "div" {:key 1} "bar"]))) 143 | (is (= 1 (util/react-key-from-vec [:r> "div" #js {:key 1} "bar"]))) 144 | 145 | ;; TODO: What should happen in this case? 146 | (is (= 1 (util/react-key-from-vec [foo {:key 1}]))) 147 | 148 | (is (= nil (util/react-key-from-vec [:r> "div" nil "bar"]))) 149 | 150 | (testing "false as key" 151 | (is (= false (util/react-key-from-vec ^{:key false} [:foo "bar"]))) 152 | (is (= false (util/react-key-from-vec [:foo {:key false} "bar"]))) 153 | (is (= false (util/react-key-from-vec [:> "div" {:key false} "bar"]))) 154 | (is (= false (util/react-key-from-vec [:f> "div" {:key false} "bar"]))) 155 | (is (= false (util/react-key-from-vec [:r> "div" #js {:key false} "bar"]))))) 156 | -------------------------------------------------------------------------------- /test/reagenttest/performancetest.cljs: -------------------------------------------------------------------------------- 1 | (ns reagenttest.performancetest 2 | (:require [reagent.core :as r] 3 | [reagent.impl.template :as tmpl] 4 | [reagent.impl.protocols :as p])) 5 | 6 | (defn hello-world-component [] 7 | [:h1 "Hello world"]) 8 | 9 | (def functional-compiler (r/create-compiler {:function-components true})) 10 | 11 | (defn test-functional [] 12 | (js/performance.mark "functional-start") 13 | ; (simple-benchmark [x [hello-world-component]] (p/as-element functional-compiler x) 100000) 14 | (dotimes [i 100000] 15 | (p/as-element functional-compiler [hello-world-component])) 16 | (js/performance.mark "functional-end") 17 | (js/console.log (js/performance.measure "functional" "functional-start" "functional-end"))) 18 | 19 | (defn test-class [] 20 | (js/performance.mark "class-start") 21 | ; (simple-benchmark [x [hello-world-component]] (p/as-element tmpl/default-compiler* x) 100000) 22 | (dotimes [i 100000] 23 | (p/as-element tmpl/class-compiler [hello-world-component])) 24 | (js/performance.mark "class-end") 25 | (js/console.log (js/performance.measure "class" "class-start" "class-end"))) 26 | 27 | (defn test-create-element [] 28 | (test-functional) 29 | (test-class)) 30 | 31 | (comment 32 | (test-create-element)) 33 | -------------------------------------------------------------------------------- /test/reagenttest/runtests.cljs: -------------------------------------------------------------------------------- 1 | (ns reagenttest.runtests 2 | (:require [reagenttest.testreagent] 3 | [reagenttest.testcursor] 4 | [reagenttest.testratom] 5 | [reagenttest.testratomasync] 6 | [reagenttest.testtrack] 7 | [reagenttest.testwithlet] 8 | [reagenttest.testwrap] 9 | [reagenttest.performancetest] 10 | [reagent.impl.template-test] 11 | [reagent.impl.util-test] 12 | [clojure.test :as test] 13 | [doo.runner :as doo :include-macros true] 14 | [jx.reporter.karma :as karma] 15 | [reagent.core :as r])) 16 | 17 | (enable-console-print!) 18 | 19 | (def test-results (r/atom nil)) 20 | 21 | (def test-box-style {:position 'absolute 22 | :margin-left -35 23 | :color :#aaa}) 24 | 25 | (defn all-tests [] 26 | (test/run-all-tests #"(reagenttest\.test.*|reagent\..*-test)")) 27 | 28 | (defmethod test/report [::test/default :summary] [m] 29 | ;; ClojureScript 2814 doesn't return anything from run-tests 30 | (reset! test-results m) 31 | (println "\nRan" (:test m) "tests containing" 32 | (+ (:pass m) (:fail m) (:error m)) "assertions.") 33 | (println (:fail m) "failures," (:error m) "errors.")) 34 | 35 | (defn run-tests [] 36 | (reset! test-results nil) 37 | (if r/is-client 38 | (js/setTimeout all-tests 100) 39 | (all-tests))) 40 | 41 | (defn test-output-mini [] 42 | (let [res @test-results] 43 | [:div 44 | {:style test-box-style} 45 | [:div {:on-click run-tests} 46 | (if res 47 | (if (zero? (+ (:fail res) (:error res))) 48 | "All tests ok" 49 | [:span "Test failure: " 50 | (:fail res) " failures, " (:error res) " errors."]) 51 | "testing")] 52 | [:button 53 | {:on-click (fn [_e] 54 | (reagenttest.performancetest/test-create-element))} 55 | "Run performance test"]])) 56 | 57 | (defn init! [] 58 | ;; This function is only used when running tests from the demo app. 59 | ;; Which is why exit-point is set manually. 60 | (when (some? (test/deftest empty-test)) 61 | (doo/set-exit-point! (fn [success?] nil)) 62 | (run-tests) 63 | [#'test-output-mini])) 64 | 65 | ;; From cognitect-labs/test-runner. 66 | ;; Will modify test vars based on filter-fn, based on 67 | ;; the metadata on test vars, so that 68 | ;; test run in cljs.test will not see ignored test vars. 69 | (defn filter-vars! [ns-syms filter-fn] 70 | (doseq [ns-sym ns-syms] 71 | (doseq [[_ var] ns-sym] 72 | (when (:test (meta var)) 73 | (when (not (filter-fn var)) 74 | (set! (.-cljs$lang$test @var) nil)))))) 75 | 76 | (goog-define ^boolean DOM_TESTS true) 77 | 78 | (when (false? DOM_TESTS) 79 | (js/console.log "DOM tests disabled") 80 | ;; Filter vars on namespaces using ^:dom metadata on test vars. 81 | (filter-vars! [(ns-publics 'reagenttest.testreagent) 82 | (ns-publics 'reagenttest.testwrap)] 83 | (fn [var] (not (:dom (meta var)))))) 84 | 85 | ;; Macro which sets *main-cli-fn* 86 | (doo/doo-all-tests #"(reagenttest\.test.*|reagent\..*-test)") 87 | 88 | (defn ^:export karma-tests [karma] 89 | (karma/run-all-tests karma #"(reagenttest\.test.*|reagent\..*-test)")) 90 | 91 | (when (exists? js/window) 92 | (when-let [f (some-> js/window .-__karma__ .-loaded_real)] 93 | (.loaded_real (.-__karma__ js/window)))) 94 | -------------------------------------------------------------------------------- /test/reagenttest/utils.clj: -------------------------------------------------------------------------------- 1 | (ns reagenttest.utils) 2 | 3 | (defmacro deftest [test-name & body] 4 | `(do 5 | (cljs.test/deftest 6 | ~(with-meta (symbol (str (name test-name) "--class")) 7 | (meta test-name)) 8 | (binding [reagenttest.utils/*test-compiler* class-compiler 9 | reagenttest.utils/*test-compiler-name* "class"] 10 | ~@body)) 11 | (cljs.test/deftest 12 | ~(with-meta (symbol (str (name test-name) "--fn")) 13 | (meta test-name)) 14 | (binding [reagenttest.utils/*test-compiler* fn-compiler 15 | reagenttest.utils/*test-compiler-name* "fn"] 16 | ~@body)))) 17 | 18 | (defmacro act 19 | [& body] 20 | `(reagenttest.utils/act* (fn [] ~@body))) 21 | 22 | ;; Inspired by 23 | ;; https://github.com/henryw374/Cljs-Async-Timeout-Tests/blob/master/src/widdindustries/timeout_test.cljc 24 | (defmacro async 25 | "Similar to cljs.test/async, but presumes the body 26 | will return single promise and the test is considered done 27 | when the promise is resolved. 28 | 29 | If promise isn't resolved in given timeout (default 10 seconds), 30 | test is considered timed out. 31 | 32 | First argument can be a map with options: 33 | 34 | - :timeout - default 10000 ms" 35 | [& [maybe-opts :as args]] 36 | (let [[opts body] (if (map? maybe-opts) 37 | [maybe-opts (rest args)] 38 | [nil args]) 39 | timeout-ms (:timeout opts 10000)] 40 | `(cljs.test/async done# 41 | (let [timeout# (js/setTimeout (fn [] 42 | (cljs.test/is (= 1 0) "Test timed out") 43 | (done#)) 44 | ~timeout-ms)] 45 | (try 46 | (-> (promesa.core/do ~@body) 47 | (.then (fn [] 48 | (js/clearTimeout timeout#) 49 | (done#))) 50 | (.catch (fn [e#] 51 | (js/clearTimeout timeout#) 52 | (js/console.error "Promise failed in async macro" e#) 53 | (cljs.test/is (not e#)) 54 | (done#)))) 55 | (catch js/Error e# 56 | (js/clearTimeout timeout#) 57 | (js/console.error "Error in async macro" e#) 58 | (cljs.test/is (not e#)) 59 | (done#))))))) 60 | 61 | (defmacro with-render [[sym comp] & body] 62 | (let [[opts body] (if (map? (first body)) 63 | [(first body) (rest body)] 64 | [nil body])] 65 | `(reagenttest.utils/with-render* ~comp ~opts (fn [~sym] (promesa.core/do ~@body))))) 66 | -------------------------------------------------------------------------------- /test/reagenttest/utils.cljs: -------------------------------------------------------------------------------- 1 | (ns reagenttest.utils 2 | (:require-macros reagenttest.utils) 3 | (:require [promesa.core :as p] 4 | [reagent.core :as r] 5 | [reagent.debug :as debug] 6 | [reagent.dom :as rdom] 7 | [reagent.dom.server :as server] 8 | [reagent.impl.template :as tmpl])) 9 | 10 | ;; Should be only set for tests.... 11 | ;; (set! (.-IS_REACT_ACT_ENVIRONMENT js/window) true) 12 | 13 | ;; Silence ReactDOM.render warning 14 | (defonce original-console-error (.-error js/console)) 15 | 16 | (set! (.-error js/console) 17 | (fn [& [first-arg :as args]] 18 | (cond 19 | (and (string? first-arg) (.startsWith first-arg "Warning: ReactDOM.render is no longer supported in React 18.")) 20 | nil 21 | 22 | (and (string? first-arg) (.startsWith first-arg "Warning: The current testing environment is not configured to support")) 23 | nil 24 | 25 | :else 26 | (apply original-console-error args)))) 27 | 28 | ;; The code from deftest macro will refer to these 29 | (def class-compiler tmpl/class-compiler) 30 | (def fn-compiler (r/create-compiler {:function-components true})) 31 | 32 | (def ^:dynamic *test-compiler* nil) 33 | (def ^:dynamic *test-compiler-name* nil) 34 | 35 | (defn as-string [comp] 36 | (server/render-to-static-markup comp *test-compiler*)) 37 | 38 | ;; For testing logged errors and warnings 39 | 40 | (defn log-error [& f] 41 | (debug/error (apply str f))) 42 | 43 | ;; "Regular versions" 44 | 45 | (defn wrap-capture-console-error [f] 46 | (fn [] 47 | (let [org js/console.error] 48 | (set! js/console.error log-error) 49 | (try 50 | (f) 51 | (finally 52 | (set! js/console.error org)))))) 53 | 54 | (defn init-capture [] 55 | (let [org-console js/console.error 56 | org-window js/window.onerror 57 | l (fn [e] 58 | (log-error e))] 59 | ;; console.error 60 | (set! js/console.error log-error) 61 | ;; reagent.debug 62 | (set! debug/tracking true) 63 | (reset! debug/warnings nil) 64 | ;; window.error 65 | (if (exists? js/window) 66 | (set! js/window.onerror (fn [e] 67 | (log-error e) 68 | true)) 69 | (let [process (js/require "process")] 70 | (.on process "uncaughtException" l))) 71 | (fn [] 72 | (set! js/console.error org-console) 73 | (reset! debug/warnings nil) 74 | (set! debug/tracking false) 75 | (if (exists? js/window) 76 | (set! js/window.onerror org-window) 77 | (let [process (js/require "process")] 78 | (.removeListener process "uncaughtException" l)))))) 79 | 80 | (defn act* 81 | "Run f to trigger Reagent updates, 82 | will return Promise which will resolve after 83 | Reagent and React render." 84 | [f] 85 | (let [p (p/deferred)] 86 | (f) 87 | (r/flush) 88 | (r/after-render (fn [] 89 | (p/resolve! p))) 90 | p)) 91 | 92 | (def ^:dynamic *render-error* nil) 93 | 94 | (defn with-render* 95 | "Render the given component to a DOM node, 96 | after the the component is mounted to DOM, 97 | run the given function and wait for the Promise 98 | returned from the function to be resolved 99 | before unmounting the component from DOM." 100 | ([comp f] 101 | (with-render* comp *test-compiler* f)) 102 | ([comp options f] 103 | (let [div (.createElement js/document "div") 104 | first-render (p/deferred) 105 | callback (fn [] 106 | (p/resolve! first-render)) 107 | compiler (:compiler options) 108 | restore-error-handlers (when (:capture-errors options) 109 | (init-capture)) 110 | ;; Magic setup to make exception from render available to the 111 | ;; with-render body. 112 | render-error (atom nil)] 113 | (try 114 | (if compiler 115 | (rdom/render comp div {:compiler compiler 116 | :callback callback}) 117 | (rdom/render comp div callback)) 118 | (catch :default e 119 | (reset! render-error e) 120 | nil)) 121 | (-> first-render 122 | ;; The callback is called even if render throws an error, 123 | ;; so this is always resolved. 124 | (p/then (fn [] 125 | (p/do 126 | (set! *render-error* @render-error) 127 | (f div) 128 | (set! *render-error* nil)))) 129 | ;; If f throws more errors, just ignore them? 130 | ;; Not sure if this makes sense. 131 | (p/catch (fn [] nil)) 132 | (p/then (fn [] 133 | (rdom/unmount-component-at-node div) 134 | ;; Need to wait for reagent tick after unmount 135 | ;; for the ratom watches to be removed? 136 | (let [ratoms-cleaned (p/deferred)] 137 | (r/next-tick (fn [] 138 | (p/resolve! ratoms-cleaned))) 139 | ratoms-cleaned))) 140 | (p/finally (fn [] 141 | (when restore-error-handlers 142 | (restore-error-handlers)))))))) 143 | -------------------------------------------------------------------------------- /test/runners/karma.conf.js: -------------------------------------------------------------------------------- 1 | /* jshint strict: false */ 2 | /* globals configData */ 3 | 4 | /* 5 | * Doo reads this file from classpath runners/karma.conf.js 6 | * This sets up junit reporter. 7 | */ 8 | 9 | var path = require('path'); 10 | 11 | module.exports = function(config) { 12 | 13 | var suite = path.basename(process.cwd()); 14 | 15 | configData.plugins = ['karma-*']; 16 | 17 | configData.logLevel = config.LOG_WARN; 18 | 19 | configData.reporters = ['dots', 'junit']; 20 | configData.junitReporter = { 21 | outputDir: (process.env.CIRCLE_TEST_REPORTS || 'junit'), 22 | outputFile: suite + '.xml', 23 | suite: suite, // suite will become the package name attribute in xml testsuite element 24 | useBrowserName: false // add browser name to report and classes names 25 | }; 26 | 27 | if (process.env.COVERAGE) { 28 | configData.reporters = ['dots', 'junit', 'coverage']; 29 | 30 | configData.preprocessors = { 31 | 'target/cljsbuild/test/out/reagent/**/!(*_test).js': ['sourcemap', 'coverage'], 32 | }; 33 | 34 | configData.coverageReporter = { 35 | reporters: [ 36 | {type: 'html'}, 37 | {type: 'lcovonly'}, 38 | ], 39 | dir: 'coverage', 40 | subdir: '.', 41 | includeAllSources: true, 42 | }; 43 | } 44 | 45 | config.set(configData); 46 | }; 47 | --------------------------------------------------------------------------------