├── .circleci └── config.yml ├── .clj-kondo └── config.edn ├── .github ├── FUNDING.yml └── workflows │ └── test.yml ├── .gitignore ├── .idea └── runConfigurations │ └── CLJ.xml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── PORTING-FROM-2.x.adoc ├── README.adoc ├── build.clj ├── deps.edn ├── docs ├── CNAME ├── Gemfile ├── README-website.md ├── _config.yml ├── _includes │ ├── footer.html │ └── header.html ├── _layouts │ ├── default.html │ ├── home.html │ └── post.html ├── _posts │ └── 2017-12-26-remotes-as-an-abstraction.md ├── adstage.png ├── assets │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── browserconfig.xml │ ├── css │ │ ├── style.scss │ │ └── syntax.css │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── img │ │ ├── adstage.png │ │ ├── atlascrm.png │ │ ├── bg.png │ │ ├── client-network-topo.png │ │ ├── cmcc_logo.svg │ │ ├── data-tree.png │ │ ├── datomic-norm.png │ │ ├── dave.png │ │ ├── dbgraph.png │ │ ├── dbmodel.png │ │ ├── figshare-logo.png │ │ ├── fulcro-letters.svg │ │ ├── graph-query-abstract.png │ │ ├── icon-arrow.svg │ │ ├── icon-complexity.svg │ │ ├── icon-ff.svg │ │ ├── logo.svg │ │ ├── network-topo.png │ │ ├── pyroclast-w75-rt.svg │ │ ├── queryidentoperation.png │ │ ├── rendering.png │ │ ├── server-interactions.png │ │ ├── sql-norm.png │ │ └── ui-graph.png │ ├── mstile-150x150.png │ ├── safari-pinned-tab.svg │ └── site.webmanifest ├── benefits.md ├── blog.md ├── bootstrap-3.3.7 │ ├── css │ │ ├── bootstrap-theme.min.css │ │ └── bootstrap.min.css │ └── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 ├── code-style.css ├── codemirror.css ├── docs.md ├── edn.css ├── evaluation.md ├── fund.md ├── help.md ├── index.html ├── js │ └── tutorial.js ├── logo.png ├── logo.svg └── svg │ ├── app-database-diagram.svg │ ├── modular-server.svg │ ├── query-demo-query-tree.svg │ ├── query-demo-ui-tree.svg │ └── ui-tree-dependency-graph.svg ├── guardrails-test.edn ├── guardrails.edn ├── karma.conf.js ├── package.json ├── pom.xml ├── resources └── public │ ├── base.css │ ├── hooks.html │ ├── index.css │ ├── index.html │ ├── roots.html │ └── todo.html ├── shadow-cljs.edn ├── src ├── clj-kondo │ └── clj-kondo.exports │ │ └── com.fulcrologic │ │ └── fulcro │ │ ├── com │ │ └── fulcrologic │ │ │ └── fulcro │ │ │ └── clj_kondo_hooks.clj │ │ └── config.edn ├── dev │ └── user.clj ├── main │ ├── com │ │ └── fulcrologic │ │ │ └── fulcro │ │ │ ├── algorithms │ │ │ ├── data_targeting.cljc │ │ │ ├── denormalize.cljc │ │ │ ├── do_not_use.cljc │ │ │ ├── form_state.cljc │ │ │ ├── indexing.cljc │ │ │ ├── lookup.cljc │ │ │ ├── merge.cljc │ │ │ ├── normalize.cljc │ │ │ ├── normalized_state.cljc │ │ │ ├── react_interop.cljc │ │ │ ├── scheduling.cljc │ │ │ ├── server_render.cljc │ │ │ ├── tempid.cljc │ │ │ ├── timbre_support.cljs │ │ │ ├── transit.cljc │ │ │ ├── tx-processing-readme.adoc │ │ │ ├── tx_processing.cljc │ │ │ ├── tx_processing │ │ │ │ ├── batched_processing.cljc │ │ │ │ └── synchronous_tx_processing.cljc │ │ │ └── tx_processing_debug.cljc │ │ │ ├── application.cljc │ │ │ ├── components.cljc │ │ │ ├── data_fetch.cljc │ │ │ ├── dom.clj │ │ │ ├── dom.cljs │ │ │ ├── dom │ │ │ ├── DOMInputs.adoc │ │ │ ├── events.cljc │ │ │ ├── html_entities.cljc │ │ │ ├── icons.cljc │ │ │ └── inputs.cljc │ │ │ ├── dom_common.cljc │ │ │ ├── dom_server.clj │ │ │ ├── inspect │ │ │ ├── devtool_api.cljc │ │ │ ├── diff.cljc │ │ │ ├── dom_picker_preload.cljs │ │ │ ├── element_picker.cljc │ │ │ ├── inspect_client.cljc │ │ │ ├── preload.cljs │ │ │ ├── target_impl.cljc │ │ │ ├── tools.cljc │ │ │ ├── transit.cljs │ │ │ └── websocket_preload.cljs │ │ │ ├── mutations.cljc │ │ │ ├── networking │ │ │ ├── file_upload.clj │ │ │ ├── file_upload.cljs │ │ │ ├── file_url.cljc │ │ │ ├── http_remote.cljs │ │ │ ├── mock_server_remote.cljs │ │ │ └── tenacious_remote.cljs │ │ │ ├── offline │ │ │ ├── browser_edn_store.cljs │ │ │ ├── durable_edn_store.cljc │ │ │ ├── durable_mutations.cljc │ │ │ ├── load_cache.cljc │ │ │ └── tempid_strategy.cljc │ │ │ ├── raw │ │ │ ├── application.cljc │ │ │ └── components.cljc │ │ │ ├── react │ │ │ ├── context.cljc │ │ │ ├── error_boundaries.cljc │ │ │ ├── hooks.cljc │ │ │ └── version18.cljc │ │ │ ├── rendering │ │ │ ├── ident_optimized_render.cljc │ │ │ ├── keyframe_render.cljc │ │ │ ├── keyframe_render2.cljc │ │ │ └── multiple_roots_renderer.cljc │ │ │ ├── routing │ │ │ ├── dynamic_routing.cljc │ │ │ └── legacy_ui_routers.cljc │ │ │ ├── server │ │ │ ├── api_middleware.clj │ │ │ └── config.clj │ │ │ ├── specs.cljc │ │ │ └── ui_state_machines.cljc │ └── data_readers.clj ├── test │ ├── com │ │ └── fulcrologic │ │ │ └── fulcro │ │ │ ├── algorithms │ │ │ ├── data_targeting_spec.clj │ │ │ ├── denormalize_spec.cljc │ │ │ ├── form_state_spec.cljc │ │ │ ├── indexing_spec.cljc │ │ │ ├── legacy_db_tree.cljc │ │ │ ├── merge_spec.cljc │ │ │ ├── normalize_spec.cljc │ │ │ ├── normalized_state_spec.cljc │ │ │ ├── transit_spec.cljc │ │ │ └── tx_processing_spec.cljc │ │ │ ├── application_spec.cljc │ │ │ ├── component_dynamic_query_spec.cljc │ │ │ ├── components_spec.cljc │ │ │ ├── data_fetch_spec.cljc │ │ │ ├── dom_server_spec.clj │ │ │ ├── dom_spec.cljc │ │ │ ├── macros │ │ │ ├── defmutation_spec.clj │ │ │ └── defsc_spec.clj │ │ │ ├── mutations_spec.cljc │ │ │ ├── offline │ │ │ └── write_through_mutations_test.cljc │ │ │ ├── raw │ │ │ └── components_spec.cljc │ │ │ ├── routing │ │ │ ├── dynamic_routing_test.cljc │ │ │ └── legacy_ui_routers_spec.cljc │ │ │ ├── server │ │ │ ├── config_spec.clj │ │ │ └── dont_require_me.clj │ │ │ └── ui_state_machines_spec.cljc │ └── config │ │ ├── defaults.edn │ │ ├── other.edn │ │ └── test.edn ├── todomvc │ ├── fulcro_todomvc │ │ ├── api.cljs │ │ ├── app.cljs │ │ ├── custom_types.cljc │ │ ├── main.cljs │ │ ├── playground.cljs │ │ ├── server.clj │ │ ├── server.cljs │ │ ├── ui.cljs │ │ ├── ui_with_legacy_ui_routers.cljs │ │ └── websocket_server.clj │ └── other_demos │ │ ├── hooks_demo.cljs │ │ └── multi_root_sample.cljs └── workspaces │ └── com │ └── fulcrologic │ └── fulcro │ ├── cards │ ├── composition3_cards.cljs │ ├── composition4_cards.cljs │ ├── composition_cards.cljs │ ├── data_view_cards.cljs │ ├── dynamic_recursion_cards.cljs │ ├── error_boundary_cards.cljs │ ├── form_cards.cljs │ ├── hooks_demo_cards.cljs │ ├── multi_root_cards.cljs │ ├── nested_dynamic_routing_tree_cards.cljs │ ├── path_ordered_routing_cards.cljs │ ├── react_hooks_cards.cljs │ ├── ref_cards.cljs │ └── source_annotation_cards.cljs │ └── issues │ ├── issue431_computed_sync_tx_cards.cljs │ └── pr552_string_buffered_input_cards.cljs ├── tests.edn └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | orbs: 3 | browser-tools: circleci/browser-tools@1.5.3 4 | jobs: 5 | cljs: 6 | docker: 7 | - image: cimg/clojure:1.10.3-browsers 8 | steps: 9 | - checkout 10 | - browser-tools/install-browser-tools 11 | - restore_cache: 12 | key: cljs-{{ checksum "deps.edn" }}-{{ checksum "package.json" }} 13 | - run: npm install 14 | - run: npx shadow-cljs -A:dev -v compile ci-tests 15 | - run: ls -l target 16 | - run: npx karma start --single-run 17 | - save_cache: 18 | paths: 19 | - node_modules 20 | - ~/.m2 21 | key: cljs-{{ checksum "deps.edn" }}-{{ checksum "package.json" }} 22 | clj: 23 | docker: 24 | - image: cimg/clojure:1.10.3 25 | steps: 26 | - checkout 27 | - restore_cache: 28 | key: clj-{{ checksum "deps.edn" }} 29 | - run: clojure -A:dev:test:clj-tests -J-Dguardrails.enabled=true -J-Dguardrails.config=guardrails-test.edn 30 | - save_cache: 31 | paths: 32 | - ~/.m2 33 | key: clj-{{ checksum "deps.edn" }} 34 | workflows: 35 | version: 2 36 | fullstack: 37 | jobs: 38 | - clj 39 | - cljs 40 | -------------------------------------------------------------------------------- /.clj-kondo/config.edn: -------------------------------------------------------------------------------- 1 | {:config-paths ["../src/clj-kondo/clj-kondo.exports/com.fulcrologic/fulcro"] 2 | :lint-as {dataico.rad.entity-snapshot/def-snapshot-ref clojure.core/def}} 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: awkay 2 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Clojure CI 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: actions/setup-java@v2 11 | with: 12 | distribution: 'adopt' 13 | java-version: 11 14 | - name: Setup Clojure 15 | uses: DeLaGuardo/setup-clojure@master 16 | with: 17 | tools-deps: '1.10.3.1040' 18 | - name: Cache All The Things 19 | uses: actions/cache@v4 20 | with: 21 | path: | 22 | ~/.m2/repository 23 | ~/.gitlibs 24 | ~/.clojure 25 | ~/.cpcache 26 | key: ${{ runner.os }}-${{ hashFiles('**/deps.edn') }} 27 | - name: Run Tests 28 | run: clojure -M:test:clj-tests 29 | 30 | - run: clojure -T:build jar 31 | - name: Check CljDoc 32 | uses: cljdoc/cljdoc-check-action@v0.0.3 33 | 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.iml 3 | *.log 4 | *.sw? 5 | *.swp 6 | *jar 7 | .DS_Store 8 | .cljs_rhino_repl 9 | .idea 10 | .lein* 11 | .lein-deps-sum 12 | .lein-env 13 | .lein-failures 14 | .lein-plugins/ 15 | .lein-repl-history 16 | .nrepl* 17 | .nrepl-port 18 | .repl 19 | bin/publish-local 20 | checkouts 21 | classes 22 | compiled 23 | datahub.log* 24 | docs/.asciidoctor/ 25 | docs/basic-db.png 26 | docs/mutations.png 27 | examples/calendar/resources/public/js/specs 28 | examples/calendar/src/quiescent_model 29 | examples/todo/src/quiescent_model 30 | figwheel_server.log 31 | lib 32 | node_modules 33 | out 34 | resources/private/js 35 | resources/public/js 36 | resources/public/js/cards 37 | resources/public/js/test 38 | target 39 | resources/public/getting-started.html 40 | resources/public/.asciidoctor/ 41 | resources/public/*.png 42 | docs/_site 43 | docs/.sass-cache 44 | docs/Gemfile.lock 45 | docs/js/[a-fh-su-z]* 46 | docs/js/goog 47 | docs/js/garden 48 | old-docs/.asciidoctor 49 | old-docs/plumbing.png 50 | DevelopersGuide.html 51 | docs/.jekyll-metadata 52 | .floo 53 | .flooignore 54 | .asciidoctor 55 | /ReferenceGuide.html 56 | .cpcache 57 | .shadow-cljs 58 | resources/public/workspaces 59 | !.idea/runConfigurations 60 | .lsp 61 | .clj-kondo/.cache/ 62 | lsp 63 | .clj-kondo/com.wsscode 64 | .clj-kondo 65 | -------------------------------------------------------------------------------- /.idea/runConfigurations/CLJ.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for taking an interest in the project and your consideration of contribution. Your gesture is highly appreciated! 4 | 5 | Fulcro is an open source (family of) projects and there are a few ways in which your contribution would 6 | make a great impact. 7 | 8 | ## Financial contribution 9 | 10 | You can support Fulcro's development by considering a donation via the 11 | [Github Sponsor's program](https://github.com/sponsors/awkay). 12 | 13 | ## Code contribution 14 | 15 | NOTE: All repositories in active development now use `main` as their main branch. 16 | 17 | As a living open-source project there's always a lot which could be done. If you are just starting out with the code-base then 18 | consider making good use of the online [video-series](https://www.youtube.com/user/tonythekay/videos) where you might find videos 19 | and tutorials in addition to the information regarding the development setup and various optimizations. 20 | 21 | Also, feel free make use of the fact that most of the source code is in `CLJC` file which makes it possible to a fire up a REPL and 22 | explore things interactively. 23 | 24 | The other piece of advice is that you should get familiar with the [fulcro-spec](https://github.com/fulcrologic/fulcro-spec) testing library, 25 | which is used extensively in Fulcro. 26 | 27 | ### Github Instructions 28 | 29 | Basically follow the instructions here: 30 | 31 | https://help.github.com/categories/collaborating-with-issues-and-pull-requests/ 32 | 33 | ### Guidelines for the PR 34 | 35 | If you'd like to submit a PR, please follow these general guidelines: 36 | 37 | - Read through the current (and closed) issues on Github. There might be something you can help with! 38 | - In general, a PR is expected to be accompanied with test cases. 39 | - Either talk about it in Slack on #fulcro, or open a github issue 40 | - Please use a github issue to go along with your PR. 41 | - Fulcro adheres to the single-commit-per-PR policy, therefore please squash your change into a single commit for it to be mergeable. 42 | - Be sure to make a very detailed commit message. The first line should be a summary sentence, and then there should be 43 | a list of bullet items if more than one change was made. 44 | 45 | ### Guidelines for documentation 46 | 47 | It's a good idea for a docstring to do more than the function name itself already does 48 | (e.g. `clear-js-timeout!` does not need a docstring that says "Clears a js timeout"...it adds no clarity). 49 | 50 | For adding doc-strings, please consider the end-user (programmer) who will be using the function, and what they are going to want to know: 51 | 52 | - What are the arguments (i.e. allowed types, units, etc.). If an argument is named 53 | `tm` for example, is that an inst?, and integer? Is it measured in seconds, minutes, milliseconds? 54 | - What is the return value? 55 | - Are there other functions that they should consider/look at? 56 | - Can some things be nil? 57 | - What are the error behaviors? 58 | - How might the function behave in surprising ways? 59 | - A small example is really useful, esp. if the function is pure 60 | 61 | Also, try to format the docstring using markdown, since `cljdoc` and other tools support that. For example: 62 | 63 | ``` 64 | (defn set-js-timeout! 65 | "Create a timer that will call `f` after `tm` milliseconds. 66 | 67 | Returns an opaque value that can be passed to `clear-js-timeout!` to cancel the timer. 68 | 69 | See also `defer`, which is intended to be platform independent. 70 | " 71 | [tm f] 72 | ...) 73 | 74 | ``` 75 | 76 | Perhaps, it might not make sense to give an example usage for a small utility like the above, 77 | but for something like `integrate-ident` I certainly would recommend a more elaborate docstring. 78 | 79 | ## Community Participation 80 | 81 | Last but not the least, please connect with the friendly Fulcro community over at #fulcro channel in the `Clojurians` slack. 82 | This is a place for fulcro beginners and experts alike to hangout and share knowledge. 83 | 84 | # Welcome to the Fulcro community! 85 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2017-2019, Fulcrologic, LLC 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the 6 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 7 | persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 10 | Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 13 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 15 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | tests: 2 | yarn 3 | npx shadow-cljs -A:dev compile ci-tests 4 | npx karma start --single-run 5 | clojure -A:dev:test:clj-tests -J-Dguardrails.config=guardrails-test.edn -J-Dguardrails.enabled 6 | 7 | dev: 8 | clojure -A:dev:test:clj-tests -J-Dguardrails.config=guardrails-test.edn -J-Dguardrails.enabled --watch --fail-fast --no-capture-output 9 | 10 | deploy: 11 | rm -rf target 12 | mvn deploy 13 | 14 | check-clj-doc: 15 | clojure -T:build jar 16 | clojure -T:check-clj-doc analyze-local -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | :source-highlighter: coderay 2 | :source-language: clojure 3 | :toc: 4 | :toc-placement: preamble 5 | :sectlinks: 6 | :sectanchors: 7 | :sectnums: 8 | 9 | image:docs/logo.png[] 10 | 11 | image:https://img.shields.io/clojars/v/com.fulcrologic/fulcro.svg[link=https://clojars.org/com.fulcrologic/fulcro] 12 | image:https://circleci.com/gh/fulcrologic/fulcro/tree/main.svg?style=svg["CircleCI", link="https://circleci.com/gh/fulcrologic/fulcro/tree/main"] 13 | 14 | Fulcro is a library for building data-driven full-stack applications for the web, native, and desktop (via electron). It uses React and is written in 15 | Clojure and Clojurescript. 16 | 17 | == Fulcro 3.8 Important Notice 18 | 19 | Fulcro 3.8+ use a new version of Fulcro Inspect, and it requires you change how you build the development version of your app. Old versions of Fulcro Inspect Electron and Chrome will not work with Fulcro 3.8+. 20 | 21 | The rewrite of Fulcro Inspect is available via the releases page of https://github.com/fulcrologic/fulcro-inspect/releases[Fulcro Inspect]. And there are preliminary instructions for using it with the latest Fulcro. 22 | 23 | The alpha versions of Fulcro 3.8 are alpha purely because of this development-time change and its potential issues, and are otherwise production ready. 24 | 25 | == Trying it Out 26 | 27 | The documentation for this version is in the http://book.fulcrologic.com/[Developer's Guide]. If you're using 28 | Fulcro 2, you can still read the http://book.fulcrologic.com/fulcro2[prior version of the guide]. 29 | 30 | There is also a https://github.com/fulcrologic/fulcro-template[template project] which you can use as a starting point. 31 | 32 | Finally, there is a plenty of great resources collected at the https://fulcro-community.github.io/[Fulcro Community] site. 33 | 34 | == Contributing to Fulcro 35 | 36 | For learning more about how to contribute to the Fulcro project, please refer 37 | https://github.com/fulcrologic/fulcro/blob/main/CONTRIBUTING.md[CONTRIBUTING.md] 38 | 39 | == Copyright and License 40 | 41 | Fulcro is: 42 | 43 | Copyright (c) 2017-2022, Fulcrologic, LLC 44 | The MIT License (MIT) 45 | 46 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 47 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the 48 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 49 | persons to whom the Software is furnished to do so, subject to the following conditions: 50 | 51 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 52 | Software. 53 | 54 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 55 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 56 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 57 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 58 | -------------------------------------------------------------------------------- /build.clj: -------------------------------------------------------------------------------- 1 | (ns build 2 | "Fulcro's build script 3 | 4 | clojure -A:build -T:build jar 5 | 6 | For more information, run: 7 | 8 | clojure -A:deps -T:build help/doc" 9 | (:require 10 | [clojure.edn :as edn] 11 | [org.corfield.build :as bb])) 12 | 13 | (def src-dirs (:paths (edn/read-string (slurp "deps.edn")))) 14 | 15 | (def lib 'com.fulcrologic/fulcro) 16 | (def version "snapshot") 17 | 18 | (defn jar "Build the JAR." [opts] 19 | (-> opts 20 | (bb/clean) 21 | (assoc :lib lib :version version 22 | :src-dirs src-dirs 23 | :resource-dirs []) 24 | (bb/jar))) 25 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src/main" 2 | ;; include clj-kondo exports in the JAR so that they can be imported, 3 | ;; see https://github.com/clj-kondo/clj-kondo/blob/master/doc/config.md#exporting-and-importing-configuration 4 | "src/clj-kondo"] 5 | 6 | :deps {edn-query-language/eql {:mvn/version "1.0.2"} 7 | com.taoensso/timbre {:mvn/version "6.5.0"} 8 | com.taoensso/encore {:mvn/version "3.127.0"} 9 | 10 | com.cognitect/transit-clj {:mvn/version "1.0.329"} 11 | com.cognitect/transit-cljs {:mvn/version "0.8.280"} 12 | com.fulcrologic/guardrails {:mvn/version "1.2.9"} 13 | 14 | org.clojure/clojure {:mvn/version "1.11.3" :scope "provided"} 15 | org.clojure/clojurescript {:mvn/version "1.11.132" :scope "provided"} 16 | org.clojure/core.async {:mvn/version "1.3.610"}} 17 | 18 | :aliases {:test {:extra-paths ["src/test"] 19 | :extra-deps {org.clojure/test.check {:mvn/version "1.1.1"} 20 | com.wsscode/pathom {:mvn/version "2.3.1"} 21 | fulcrologic/fulcro-spec {:mvn/version "3.1.12" 22 | :exclusions [com.fulcrologic/fulcro]}}} 23 | 24 | :clj-tests {:extra-paths ["src/test"] 25 | :main-opts ["-m" "kaocha.runner"] 26 | :extra-deps {lambdaisland/kaocha {:mvn/version "1.65.1029"}}} 27 | 28 | :workspaces {:extra-paths ["src/workspaces"] 29 | :extra-deps {com.github.awkay/workspaces {:mvn/version "1.0.3" 30 | :exclusions [com.fulcrologic/fulcro]}}} 31 | 32 | :dev {:extra-paths ["src/dev" "resources" "src/todomvc"] 33 | :extra-deps {thheller/shadow-cljs {:mvn/version "2.28.20"} 34 | com.wsscode/pathom {:mvn/version "2.3.1"} 35 | com.fulcrologic/fulcro-websockets {:mvn/version "3.2.0" 36 | :exclusions [com.fulcrologic/fulcro]} 37 | com.fulcrologic/fulcro-inspect {:mvn/version "1.0.1"} 38 | binaryage/devtools {:mvn/version "1.0.6"} 39 | ring/ring-core {:mvn/version "1.8.1"} 40 | org.immutant/web {:mvn/version "2.1.10"} 41 | org.clojure/tools.namespace {:mvn/version "1.0.0"}}} 42 | 43 | :build {:deps {io.github.seancorfield/build-clj {:tag "v0.6.7" :sha "22c2d09"}} 44 | :ns-default build} 45 | 46 | :check-clj-doc {:deps {io.github.cljdoc/cljdoc-analyzer {:tag "v1.0.802" :sha "911656c5"}} 47 | :ns-default cljdoc-analyzer.deps-tool}}} -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | fulcro.fulcrologic.com -------------------------------------------------------------------------------- /docs/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # Hello! This is where you manage which Jekyll version is used to run. 4 | # When you want to use a different version, change it below, save the 5 | # file and run `bundle install`. Run Jekyll with `bundle exec`, like so: 6 | # 7 | # bundle exec jekyll serve 8 | # 9 | # This will help ensure the proper Jekyll version is running. 10 | # Happy Jekylling! 11 | 12 | # If you want to use GitHub Pages, remove the "gem "jekyll"" above and 13 | # uncomment the line below. To upgrade, run `bundle update github-pages`. 14 | # gem "github-pages", group: :jekyll_plugins 15 | 16 | gem "github-pages", group: :jekyll_plugins 17 | -------------------------------------------------------------------------------- /docs/README-website.md: -------------------------------------------------------------------------------- 1 | # Editing the Website 2 | 3 | These instructions assume a UNIX-like (OSX in particular) OS. 4 | 5 | You should be able to get a local rendering system going to preview your 6 | edits by following these instructions: 7 | 8 | 1. Make sure you have xcode installed with command line tools `xcodeselect --install`. 9 | 2. Use homebrew to install Ruby and gem support `brew install ruby` 10 | 3. [Install Jekyll](https://jekyllrb.com/docs/installation/) 11 | 12 | Then you should be able to: 13 | 14 | ```bash 15 | $ cd docs 16 | $ bundle install 17 | $ bundle exec jekyll serve --incremental 18 | ``` 19 | 20 | and then use the URL it gives you. It will recompile on save, but you have 21 | to reload the browser page to see changes. 22 | 23 | ## Publishing 24 | 25 | Do edits on the `develop` branch unless it is some kind of hot fix (in 26 | which case you should use git flow hotfix). 27 | 28 | The live website is auto-generated from the current `master`. So, merging 29 | and pushing to master will update the website. 30 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | name: Fulcro 2 | markdown: kramdown 3 | -------------------------------------------------------------------------------- /docs/_includes/footer.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | {% if site.google_analytics %} 9 | 17 | {% endif %} 18 | -------------------------------------------------------------------------------- /docs/_includes/header.html: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /docs/_layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | {{ site.title | default: site.github.repository_name }} by {{ site.github.owner_name }} 33 | 34 | 35 | 36 | {% include header.html %} 37 | 38 |
39 | {{ content }} 40 |
41 | 42 | {% include footer.html %} 43 | 44 | 45 | -------------------------------------------------------------------------------- /docs/_layouts/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | {{ site.title | default: site.github.repository_name }} by {{ site.github.owner_name }} 33 | 34 | 35 | 36 | 37 | {% include header.html %} 38 | 39 | {{ content }} 40 | 41 | {% include footer.html %} 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /docs/_layouts/post.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | {{ site.title | default: site.github.repository_name }} by {{ site.github.owner_name }} 34 | 35 | 36 | 37 | 38 |
39 | 40 | {% include header.html %} 41 | 42 |
43 |
44 |

45 | {{page.title}} 46 |

47 |

48 | by {{page.author}} 49 |

50 |
51 |
52 | 53 |
54 |
55 | {{ content }} 56 |
57 |
58 | 59 | {% include footer.html %} 60 | 61 |
62 | 63 | 64 | -------------------------------------------------------------------------------- /docs/adstage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fulcrologic/fulcro/5ae0bb7283eb603c6f5f5d24bbe3e7c9d28339b0/docs/adstage.png -------------------------------------------------------------------------------- /docs/assets/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fulcrologic/fulcro/5ae0bb7283eb603c6f5f5d24bbe3e7c9d28339b0/docs/assets/android-chrome-192x192.png -------------------------------------------------------------------------------- /docs/assets/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fulcrologic/fulcro/5ae0bb7283eb603c6f5f5d24bbe3e7c9d28339b0/docs/assets/android-chrome-512x512.png -------------------------------------------------------------------------------- /docs/assets/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fulcrologic/fulcro/5ae0bb7283eb603c6f5f5d24bbe3e7c9d28339b0/docs/assets/apple-touch-icon.png -------------------------------------------------------------------------------- /docs/assets/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /docs/assets/css/style.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | $blue: #5577BB; 5 | $green: #66BB33; 6 | 7 | body { 8 | font-family: 'Cambay', sans-serif; 9 | margin: 0; 10 | } 11 | 12 | .page-container { 13 | min-height: 100vh; 14 | } 15 | 16 | .green { 17 | color: $green; 18 | } 19 | 20 | .blue { 21 | color: $blue; 22 | } 23 | 24 | .grey { 25 | color: #AAAAAA; 26 | } 27 | 28 | .menu { 29 | background: #F9F9F9; 30 | } 31 | 32 | .content-container { 33 | max-width: 960px; 34 | margin: 0 auto; 35 | } 36 | 37 | .md-container { 38 | h1,h2,h3,h4,h5 {color: $green;} 39 | } 40 | 41 | a { 42 | color: $blue; 43 | text-decoration: none; 44 | 45 | &:hover { 46 | text-decoration: underline; 47 | } 48 | 49 | } 50 | 51 | .hero { 52 | border-bottom: 5px solid #5577BB; 53 | background: url('/assets/img/bg.png') no-repeat center top; 54 | background-size: cover; 55 | } 56 | 57 | .hero h1 { 58 | color: #66BB33; 59 | text-transform: uppercase; 60 | } 61 | 62 | nav ul { 63 | list-style: none; 64 | } 65 | 66 | nav ul li { 67 | display: inline-block; 68 | } 69 | 70 | .btn-primary { 71 | background: #5577BB; 72 | color: #FFF; 73 | } 74 | 75 | .btn-secondary { 76 | background: #66BB33; 77 | color: #FFF; 78 | } 79 | 80 | .bg-grey { 81 | background-color: #F9F9F9; 82 | } 83 | 84 | section { 85 | overflow: hidden; 86 | } 87 | 88 | .section-border-green { 89 | border-bottom: 5px solid #66BB33; 90 | } 91 | 92 | footer { 93 | background: #F9F9F9; 94 | min-height: 200px; 95 | border-bottom: 4px solid #5577BB; 96 | } 97 | 98 | footer ul { 99 | list-style-type: none; 100 | } 101 | 102 | .md-content { 103 | h1, h2, h3, h4, h5 { 104 | color: $green; 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /docs/assets/css/syntax.css: -------------------------------------------------------------------------------- 1 | .highlight { background: #ffffff; } 2 | .highlight .c { color: #999988; font-style: italic } /* Comment */ 3 | .highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */ 4 | .highlight .k { font-weight: bold } /* Keyword */ 5 | .highlight .o { font-weight: bold } /* Operator */ 6 | .highlight .cm { color: #999988; font-style: italic } /* Comment.Multiline */ 7 | .highlight .cp { color: #999999; font-weight: bold } /* Comment.Preproc */ 8 | .highlight .c1 { color: #999988; font-style: italic } /* Comment.Single */ 9 | .highlight .cs { color: #999999; font-weight: bold; font-style: italic } /* Comment.Special */ 10 | .highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */ 11 | .highlight .gd .x { color: #000000; background-color: #ffaaaa } /* Generic.Deleted.Specific */ 12 | .highlight .ge { font-style: italic } /* Generic.Emph */ 13 | .highlight .gr { color: #aa0000 } /* Generic.Error */ 14 | .highlight .gh { color: #999999 } /* Generic.Heading */ 15 | .highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */ 16 | .highlight .gi .x { color: #000000; background-color: #aaffaa } /* Generic.Inserted.Specific */ 17 | .highlight .go { color: #888888 } /* Generic.Output */ 18 | .highlight .gp { color: #555555 } /* Generic.Prompt */ 19 | .highlight .gs { font-weight: bold } /* Generic.Strong */ 20 | .highlight .gu { color: #aaaaaa } /* Generic.Subheading */ 21 | .highlight .gt { color: #aa0000 } /* Generic.Traceback */ 22 | .highlight .kc { font-weight: bold } /* Keyword.Constant */ 23 | .highlight .kd { font-weight: bold } /* Keyword.Declaration */ 24 | .highlight .kp { font-weight: bold } /* Keyword.Pseudo */ 25 | .highlight .kr { font-weight: bold } /* Keyword.Reserved */ 26 | .highlight .kt { color: #445588; font-weight: bold } /* Keyword.Type */ 27 | .highlight .m { color: #009999 } /* Literal.Number */ 28 | .highlight .s { color: #d14 } /* Literal.String */ 29 | .highlight .na { color: #008080 } /* Name.Attribute */ 30 | .highlight .nb { color: #0086B3 } /* Name.Builtin */ 31 | .highlight .nc { color: #445588; font-weight: bold } /* Name.Class */ 32 | .highlight .no { color: #008080 } /* Name.Constant */ 33 | .highlight .ni { color: #800080 } /* Name.Entity */ 34 | .highlight .ne { color: #990000; font-weight: bold } /* Name.Exception */ 35 | .highlight .nf { color: #990000; font-weight: bold } /* Name.Function */ 36 | .highlight .nn { color: #555555 } /* Name.Namespace */ 37 | .highlight .nt { color: #000080 } /* Name.Tag */ 38 | .highlight .nv { color: #008080 } /* Name.Variable */ 39 | .highlight .ow { font-weight: bold } /* Operator.Word */ 40 | .highlight .w { color: #bbbbbb } /* Text.Whitespace */ 41 | .highlight .mf { color: #009999 } /* Literal.Number.Float */ 42 | .highlight .mh { color: #009999 } /* Literal.Number.Hex */ 43 | .highlight .mi { color: #009999 } /* Literal.Number.Integer */ 44 | .highlight .mo { color: #009999 } /* Literal.Number.Oct */ 45 | .highlight .sb { color: #d14 } /* Literal.String.Backtick */ 46 | .highlight .sc { color: #d14 } /* Literal.String.Char */ 47 | .highlight .sd { color: #d14 } /* Literal.String.Doc */ 48 | .highlight .s2 { color: #d14 } /* Literal.String.Double */ 49 | .highlight .se { color: #d14 } /* Literal.String.Escape */ 50 | .highlight .sh { color: #d14 } /* Literal.String.Heredoc */ 51 | .highlight .si { color: #d14 } /* Literal.String.Interpol */ 52 | .highlight .sx { color: #d14 } /* Literal.String.Other */ 53 | .highlight .sr { color: #009926 } /* Literal.String.Regex */ 54 | .highlight .s1 { color: #d14 } /* Literal.String.Single */ 55 | .highlight .ss { color: #990073 } /* Literal.String.Symbol */ 56 | .highlight .bp { color: #999999 } /* Name.Builtin.Pseudo */ 57 | .highlight .vc { color: #008080 } /* Name.Variable.Class */ 58 | .highlight .vg { color: #008080 } /* Name.Variable.Global */ 59 | .highlight .vi { color: #008080 } /* Name.Variable.Instance */ 60 | .highlight .il { color: #009999 } /* Literal.Number.Integer.Long */ 61 | -------------------------------------------------------------------------------- /docs/assets/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fulcrologic/fulcro/5ae0bb7283eb603c6f5f5d24bbe3e7c9d28339b0/docs/assets/favicon-16x16.png -------------------------------------------------------------------------------- /docs/assets/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fulcrologic/fulcro/5ae0bb7283eb603c6f5f5d24bbe3e7c9d28339b0/docs/assets/favicon-32x32.png -------------------------------------------------------------------------------- /docs/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fulcrologic/fulcro/5ae0bb7283eb603c6f5f5d24bbe3e7c9d28339b0/docs/assets/favicon.ico -------------------------------------------------------------------------------- /docs/assets/img/adstage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fulcrologic/fulcro/5ae0bb7283eb603c6f5f5d24bbe3e7c9d28339b0/docs/assets/img/adstage.png -------------------------------------------------------------------------------- /docs/assets/img/atlascrm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fulcrologic/fulcro/5ae0bb7283eb603c6f5f5d24bbe3e7c9d28339b0/docs/assets/img/atlascrm.png -------------------------------------------------------------------------------- /docs/assets/img/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fulcrologic/fulcro/5ae0bb7283eb603c6f5f5d24bbe3e7c9d28339b0/docs/assets/img/bg.png -------------------------------------------------------------------------------- /docs/assets/img/client-network-topo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fulcrologic/fulcro/5ae0bb7283eb603c6f5f5d24bbe3e7c9d28339b0/docs/assets/img/client-network-topo.png -------------------------------------------------------------------------------- /docs/assets/img/data-tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fulcrologic/fulcro/5ae0bb7283eb603c6f5f5d24bbe3e7c9d28339b0/docs/assets/img/data-tree.png -------------------------------------------------------------------------------- /docs/assets/img/datomic-norm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fulcrologic/fulcro/5ae0bb7283eb603c6f5f5d24bbe3e7c9d28339b0/docs/assets/img/datomic-norm.png -------------------------------------------------------------------------------- /docs/assets/img/dave.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fulcrologic/fulcro/5ae0bb7283eb603c6f5f5d24bbe3e7c9d28339b0/docs/assets/img/dave.png -------------------------------------------------------------------------------- /docs/assets/img/dbgraph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fulcrologic/fulcro/5ae0bb7283eb603c6f5f5d24bbe3e7c9d28339b0/docs/assets/img/dbgraph.png -------------------------------------------------------------------------------- /docs/assets/img/dbmodel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fulcrologic/fulcro/5ae0bb7283eb603c6f5f5d24bbe3e7c9d28339b0/docs/assets/img/dbmodel.png -------------------------------------------------------------------------------- /docs/assets/img/figshare-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fulcrologic/fulcro/5ae0bb7283eb603c6f5f5d24bbe3e7c9d28339b0/docs/assets/img/figshare-logo.png -------------------------------------------------------------------------------- /docs/assets/img/fulcro-letters.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 13 | 14 | 18 | 21 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /docs/assets/img/graph-query-abstract.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fulcrologic/fulcro/5ae0bb7283eb603c6f5f5d24bbe3e7c9d28339b0/docs/assets/img/graph-query-abstract.png -------------------------------------------------------------------------------- /docs/assets/img/icon-arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 11 | 13 | 14 | 16 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /docs/assets/img/icon-complexity.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 11 | 13 | 14 | 16 | 18 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /docs/assets/img/icon-ff.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 11 | 13 | 14 | 15 | 17 | 19 | 20 | 21 | 22 | 24 | 26 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /docs/assets/img/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 11 | 12 | 13 | 16 | 17 | 21 | 24 | 30 | 31 | 32 | 33 | 34 | 36 | 38 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /docs/assets/img/network-topo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fulcrologic/fulcro/5ae0bb7283eb603c6f5f5d24bbe3e7c9d28339b0/docs/assets/img/network-topo.png -------------------------------------------------------------------------------- /docs/assets/img/queryidentoperation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fulcrologic/fulcro/5ae0bb7283eb603c6f5f5d24bbe3e7c9d28339b0/docs/assets/img/queryidentoperation.png -------------------------------------------------------------------------------- /docs/assets/img/rendering.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fulcrologic/fulcro/5ae0bb7283eb603c6f5f5d24bbe3e7c9d28339b0/docs/assets/img/rendering.png -------------------------------------------------------------------------------- /docs/assets/img/server-interactions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fulcrologic/fulcro/5ae0bb7283eb603c6f5f5d24bbe3e7c9d28339b0/docs/assets/img/server-interactions.png -------------------------------------------------------------------------------- /docs/assets/img/sql-norm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fulcrologic/fulcro/5ae0bb7283eb603c6f5f5d24bbe3e7c9d28339b0/docs/assets/img/sql-norm.png -------------------------------------------------------------------------------- /docs/assets/img/ui-graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fulcrologic/fulcro/5ae0bb7283eb603c6f5f5d24bbe3e7c9d28339b0/docs/assets/img/ui-graph.png -------------------------------------------------------------------------------- /docs/assets/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fulcrologic/fulcro/5ae0bb7283eb603c6f5f5d24bbe3e7c9d28339b0/docs/assets/mstile-150x150.png -------------------------------------------------------------------------------- /docs/assets/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 26 | 32 | 37 | 39 | 44 | 46 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /docs/assets/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /docs/benefits.md: -------------------------------------------------------------------------------- 1 | # The Benefits of Fulcro 2 | 3 | In a nutshell: Fulcro eliminates a lot of incidental complexity. 4 | 5 | It allows you to think 6 | about rendering as a pure function of data, which then allows you to think about the 7 | clean evolution of your data model from one state to the next through *mutations* on that 8 | data model (atomic steps that complete one operation). The UI pretty much takes care 9 | of itself. 10 | 11 | We understand that it is hard for you to take our word for it, but library 12 | evaluation does require the reader to put in some effort. However, we also 13 | understand you're busy, so in the interest of giving you some talking points: 14 | 15 | ## Why to Use Fulcro 16 | 17 | - *Dramatically* reduce the amount of code you have to write. Fulcro apps are 18 | a *lot* smaller than the equivalent React/Relay/Redux variant. 19 | - Most asynchrony evaporates! Callbacks? We (mostly) don't need them. 20 | - Use an advanced, fast, FP language on both the client and server. 21 | - Reason about the UI as a pure function (no "bit twiddling" to modify the DOM). 22 | - Reason about the data model as a pure graph of data (mostly separate from the UI). 23 | - Data ends up in easy-to-access linked table-like structures that make understanding and updating the data easy. 24 | - Clean, unit-testable *mutations* evolve the data model. The UI takes care of itself through two easy to understand 25 | mechanisms (no two-way data binding causing (or failing to cause) storms of refreshes): 26 | - The UI refreshes anything that triggers a mutation whose data has actually changed. 27 | - Through listing (abstractly) what data in the model a mutation affects. 28 | - One langauge is used on both client and server. Like node, but without the Javascript. 29 | - A full-stack story that unifies how your model is treated on both the client and server. 30 | - It is [React](https://facebook.github.io/react/)-based: The rendering itself is done by a widely used, supported, and robust library. 31 | - The data and communication model is similar to that of GraphQL and Falcor, but simplified via a concise Datomic-like graph query language. 32 | - Read about [data driven architectures](https://medium.com/@env/demand-driven-development-relay-falcor-om-next-75818bd54ea1). 33 | - The model makes [CQRS](https://www.youtube.com/watch?v=qDNPQo9UmJA) pretty easy to add for both auditing and performance. 34 | - It has a strong FP flair: 35 | - Rendering is done as a pure function. 36 | - No in-place mutation (persistent data structures). 37 | - UI History and time travel are supported features (including a support UI VCR). 38 | - It leverages Google Closure for js optimization, so you get these for free (with little headache): 39 | - Dynamic module loading (code splitting) 40 | - Minification 41 | - Dead code elimination 42 | - A large library of reusable functions (Google's Closure library) 43 | - UI in React + cljc means that client and server-side rendering of initial loads is easy to get. 44 | - You get to think of your application almost completely as a pure data model. 45 | - A gettext-based internationalization system. 46 | - Meta-programming is quite powerful when used well (think of building a DSL that can then be used to build 47 | elements of your program). Clojure is homoiconic, making this easier. 48 | 49 | ## When not to Use Fulcro 50 | 51 | Fulcro does try to provide you with a full-stack story. It also requires that you learn 52 | (and *unlearn*) a few things that some people find initially challenging. Here are some reasons 53 | you might *not* want to use Fulcro: 54 | 55 | - You are writing a game. Fulcro shines when it comes to data-driven applications. Games typically need very fast 56 | framerates and low UI overhead. Fulcro is fast enough for data-driven apps, but it really would not make 57 | sense for animation-heavy gaming. 58 | - You don't want to learn something radically different from what you're used to. 59 | - Your co-workers don't want to learn something radically different. 60 | - Your company cannot be convinced that the long-term benefits of Fulcro will pay off in the long run compared to the 61 | costs of re-training/tooling. 62 | 63 | -------------------------------------------------------------------------------- /docs/blog.md: -------------------------------------------------------------------------------- 1 |
2 |

Available Blog Articles

3 |
4 | 5 |
6 | {% for post in site.posts %} 7 |
8 |
{{post.date | date_to_long_string}}
9 | 10 |
11 |
12 |
 
13 |
by {{post.author}}
14 |
15 | {% endfor %} 16 |
17 | -------------------------------------------------------------------------------- /docs/bootstrap-3.3.7/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fulcrologic/fulcro/5ae0bb7283eb603c6f5f5d24bbe3e7c9d28339b0/docs/bootstrap-3.3.7/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /docs/bootstrap-3.3.7/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fulcrologic/fulcro/5ae0bb7283eb603c6f5f5d24bbe3e7c9d28339b0/docs/bootstrap-3.3.7/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /docs/bootstrap-3.3.7/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fulcrologic/fulcro/5ae0bb7283eb603c6f5f5d24bbe3e7c9d28339b0/docs/bootstrap-3.3.7/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /docs/bootstrap-3.3.7/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fulcrologic/fulcro/5ae0bb7283eb603c6f5f5d24bbe3e7c9d28339b0/docs/bootstrap-3.3.7/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /docs/code-style.css: -------------------------------------------------------------------------------- 1 | @import url(https://fonts.googleapis.com/css?family=Lato); 2 | @import url(https://fonts.googleapis.com/css?family=Cutive+Mono); 3 | @import url(edn.css); 4 | 5 | .overlay { 6 | background-color: rgb(230, 230, 240); 7 | width: 194px; 8 | } 9 | 10 | .hidden { 11 | display: none; 12 | } 13 | 14 | .test-report { 15 | font-family: 'Lato' sans-serif; 16 | display: block; 17 | font-size: 20pt; 18 | } 19 | 20 | .test-item { 21 | display: block; 22 | font-size: 13pt; 23 | } 24 | 25 | .test-namespace { 26 | margin-top: 20px; 27 | font-weight: 400; 28 | display: block; 29 | font-size: 18pt; 30 | } 31 | 32 | .test-header { 33 | font-weight: 700; 34 | display: block; 35 | font-size: 18pt; 36 | } 37 | 38 | .test-manually { 39 | color: orange; 40 | } 41 | 42 | .filter-controls { 43 | position: fixed; 44 | top: 0px; 45 | right: 0px; 46 | } 47 | 48 | .filter-controls label { 49 | font-size: 10pt; 50 | } 51 | 52 | .filter-controls a { 53 | font-size: 10pt; 54 | padding-left: 5pt; 55 | } 56 | 57 | .selected { 58 | color: green; 59 | text-decoration: underline; 60 | } 61 | 62 | .test-pending { 63 | color: gray; 64 | } 65 | 66 | .test-passed { 67 | color: limegreen; 68 | } 69 | 70 | .test-error { 71 | color: red; 72 | } 73 | 74 | .test-failed { 75 | color: red; 76 | } 77 | 78 | .test-list { 79 | list-style-type: none; 80 | } 81 | 82 | .test-result { 83 | margin: 12px; 84 | font-size: 16px; 85 | } 86 | 87 | .test-result-title { 88 | width: 100px; 89 | font-size: 16px; 90 | } 91 | 92 | .test-count { 93 | margin: 20px 0 20px 20px; 94 | } 95 | 96 | .test-report ul { 97 | padding-left: 10px; 98 | margin-bottom: 10px; 99 | } 100 | 101 | .test-report ul:empty { 102 | display: none; 103 | } 104 | 105 | .test-report h2 { 106 | font-size: 24px; 107 | margin-bottom: 15px; 108 | } 109 | 110 | #test-app { 111 | display: none; 112 | } 113 | -------------------------------------------------------------------------------- /docs/docs.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | Fulcro has a number of resources that can get you going. 4 | 5 | ## Developer's Guide 6 | 7 | The [Developer's Guide](http://book.fulcrologic.com/fulcro3) covers the entire 8 | library, and also has a Getting Started chapter that is great for beginners. 9 | 10 | ## API Documentation 11 | 12 | Most functions have good doc strings that explain the purpose of the function. Use your IDE's help to see these documentation elements. 13 | -------------------------------------------------------------------------------- /docs/edn.css: -------------------------------------------------------------------------------- 1 | .rendered-edn .collection { 2 | display: flex; 3 | display: -webkit-flex; 4 | } 5 | 6 | .rendered-edn .keyval { 7 | display: flex; 8 | display: -webkit-flex; 9 | flex-wrap: wrap; 10 | -webkit-flex-wrap: wrap; 11 | } 12 | 13 | .rendered-edn .keyval > .keyword { 14 | color: #a94442; 15 | } 16 | 17 | .rendered-edn .keyval > *:first-child { 18 | margin: 0px 3px; 19 | flex-shrink: 0; 20 | -webkit-flex-shrink: 0; 21 | } 22 | 23 | .rendered-edn .keyval > *:last-child { 24 | margin: 0px 3px; 25 | } 26 | 27 | .rendered-edn .opener { 28 | color: #999; 29 | margin: 0px 4px; 30 | flex-shrink: 0; 31 | -webkit-flex-shrink: 0; 32 | } 33 | 34 | .rendered-edn .closer { 35 | display: flex; 36 | display: -webkit-flex; 37 | flex-direction: column-reverse; 38 | -webkit-flex-direction: column-reverse; 39 | margin: 0px 3px; 40 | color: #999; 41 | } 42 | 43 | .rendered-edn .string { 44 | color: #428bca; 45 | } 46 | 47 | .rendered-edn .string .opener, 48 | .rendered-edn .string .closer { 49 | display: inline; 50 | margin: 0px; 51 | color: #428bca; 52 | } 53 | -------------------------------------------------------------------------------- /docs/evaluation.md: -------------------------------------------------------------------------------- 1 | # Evaluating Fulcro (or web development tools in general) 2 | 3 | We understand that it is hard to evaluate tools. Developers spend countless 4 | hours trying to figure out what tool is right for the job at hand. 5 | 6 | I would encourage you not to just look at superficial criteria like "is 7 | it easy to get started?" 8 | Those concerns rarely affect your long-term success. Look more deeply. 9 | Try writing a simple full-stack application in each (or at least look for 10 | a full-stack example). Many things that are easy to get started with devolve 11 | into code with constructs that are fragile, difficult to 12 | reason about, and are hard to navigate (such as chains of 13 | asynchrony that can be defined by delarations in any file). 14 | 15 | One thing that has been really helpful in evaluating tech is a spreadsheet listing out 16 | the various properties that are important. Assign each 17 | a weight (a simple multiplier) to adjust for relative importance, and 18 | then have each developer blindly vote 1-10 on how well they think a given 19 | tool set solves the requirement. Average votes those together and 20 | place the result in the correct place in the spreadsheet. 21 | 22 | Here is a link to a spreadsheet you can clone in Google Docs that has 23 | many of the criteria that have a significant advantage/disadvantage 24 | in Fulcro. Add other tools you're evaluating and use if for 25 | an in-house custom a head-to-head comparison. Good Luck! 26 | 27 | [Web Tool Comparison Worksheet](https://docs.google.com/spreadsheets/d/1kBJLjN2Z1AFJM4W8S7YTn0PS_drzqtIMY5siSIZ5OWI) 28 | 29 | Instructions 30 | 1. Log in (and create if needed) to a google account 31 | 2. Open the link above to the spreadsheet 32 | 3. Choose `File -> Make a Copy` to make an editable copy 33 | 4. Read/edit/add/delete the criteria list to match things that are important to you. 34 | 5. Assign a weight to each (any number...don't limit it to 10. Something might be so critical as to need a larger number) 35 | 6. Put your other candidates across the top (where I've prewritten things like Reagent) 36 | 7. Put a score (1-10) under each tool for each criteria 37 | 38 | -------------------------------------------------------------------------------- /docs/fund.md: -------------------------------------------------------------------------------- 1 | # Support Fulcro 2 | 3 | Fulcro largely exists due to the contributions of the community. Much of the 4 | work on it is done by Tony Kay in his personal time. If you find Fulcro useful 5 | please consider showing your appreciation by contributing some of your 6 | own time or money to the project. 7 | 8 | There are a number of ways you can help: 9 | 10 | - [Become a Patron](https://www.patreon.com/fulcro). 11 | - [Send a one-time Donation](https://paypal.me/untangledfw). 12 | - Ask your employer to evaluate Fulcro, and encourage them to hire 13 | [Fulcrologic, LLC](http://www.fulcrologic.com) to help! 14 | - Report issues, ideally with a reproducible case. 15 | - Proofread the website/documentation. 16 | - Send a pull request with a fix (see [CONTRIBUTING](https://github.com/fulcrologic/fulcro/blob/develop/CONTRIBUTING.md)): 17 | - for a bug. 18 | - for a typo. 19 | - to add tests or features. 20 | 21 | -------------------------------------------------------------------------------- /docs/help.md: -------------------------------------------------------------------------------- 1 | # Getting Help 2 | 3 | If you're just getting started, please be sure you've at least 4 | tried the [documentation](docs.html). 5 | 6 | If you're still scratching your head and need someone to talk to, the best place to go 7 | is Slack. You can get a free invite via [https://clojurians.herokuapp.com/](https://clojurians.herokuapp.com/). 8 | Once you're in the [Clojurians Slack Channel](https://clojurians.slack.com/), look for us in the 9 | #fulcro channel. 10 | 11 | It is possible that someone has already asked your question, so it can be useful to search the 12 | [Clojurians Slack Archive](https://clojurians-log.clojureverse.org/). 13 | I'd recommend using the following Google Search to find past converations: 14 | 15 | ``` 16 | site:https://clojurians-log.clojureverse.org/fulcro WHAT YOU WANT TO KNOW 17 | ``` 18 | 19 | which will target your results just to our channel. 20 | 21 | ## Commercial Support and Consulting 22 | 23 | [Fulcrologic, LLC](http://www.fulcrologic.com) is a consulting company 24 | that specializes in consulting and training for Fulcro. They can provide 25 | you with services ranging from on-site skills training and collaborative design 26 | to code review and management education. 27 | -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fulcrologic/fulcro/5ae0bb7283eb603c6f5f5d24bbe3e7c9d28339b0/docs/logo.png -------------------------------------------------------------------------------- /docs/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 11 | 12 | 13 | 16 | 17 | 21 | 24 | 30 | 31 | 32 | 33 | 34 | 36 | 38 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /docs/svg/query-demo-query-tree.svg: -------------------------------------------------------------------------------- 1 | 2 |
Root
Root
Person
Person
-------------------------------------------------------------------------------- /guardrails-test.edn: -------------------------------------------------------------------------------- 1 | {:defn-macro nil 2 | :throw? true 3 | :emit-spec? true 4 | :expound {:show-valid-values? true 5 | :print-specs? true}} -------------------------------------------------------------------------------- /guardrails.edn: -------------------------------------------------------------------------------- 1 | {:defn-macro nil 2 | :throw? false 3 | :emit-spec? true 4 | :expound {:show-valid-values? true 5 | :print-specs? true}} -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | config.set({ 3 | browsers: ['ChromeHeadless'], 4 | // The directory where the output file lives 5 | basePath: 'target', 6 | // The file itself 7 | files: ['ci.js'], 8 | frameworks: ['cljs-test'], 9 | plugins: ['karma-cljs-test', 'karma-chrome-launcher'], 10 | colors: true, 11 | logLevel: config.LOG_INFO, 12 | client: { 13 | args: ["shadow.test.karma.init"], 14 | singleRun: true 15 | } 16 | }) 17 | }; 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fulcro", 3 | "version": "1.0.0", 4 | "description": "Testing", 5 | "main": "index.js", 6 | "directories": {}, 7 | "devDependencies": { 8 | "karma": "6.3.2", 9 | "karma-chrome-launcher": "3.1.0", 10 | "karma-cljs-test": "0.1.0", 11 | "react": "18.2.0", 12 | "react-dom": "18.2.0", 13 | "shadow-cljs": "2.14.5", 14 | "transit-js": "^0.8.867" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git@github.com:fulcrologic/fulcro.git" 19 | }, 20 | "author": "", 21 | "license": "MIT", 22 | "dependencies": { 23 | "highlight.js": "^10.7.2", 24 | "react-grid-layout": "^1.2.4", 25 | "yarn": "^1.22.19" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /resources/public/base.css: -------------------------------------------------------------------------------- 1 | hr { 2 | margin: 20px 0; 3 | border: 0; 4 | border-top: 1px dashed #c5c5c5; 5 | border-bottom: 1px dashed #f7f7f7; 6 | } 7 | 8 | .learn a { 9 | font-weight: normal; 10 | text-decoration: none; 11 | color: #b83f45; 12 | } 13 | 14 | .learn a:hover { 15 | text-decoration: underline; 16 | color: #787e7e; 17 | } 18 | 19 | .learn h3, 20 | .learn h4, 21 | .learn h5 { 22 | margin: 10px 0; 23 | font-weight: 500; 24 | line-height: 1.2; 25 | color: #000; 26 | } 27 | 28 | .learn h3 { 29 | font-size: 24px; 30 | } 31 | 32 | .learn h4 { 33 | font-size: 18px; 34 | } 35 | 36 | .learn h5 { 37 | margin-bottom: 0; 38 | font-size: 14px; 39 | } 40 | 41 | .learn ul { 42 | padding: 0; 43 | margin: 0 0 30px 25px; 44 | } 45 | 46 | .learn li { 47 | line-height: 20px; 48 | } 49 | 50 | .learn p { 51 | font-size: 15px; 52 | font-weight: 300; 53 | line-height: 1.3; 54 | margin-top: 0; 55 | margin-bottom: 0; 56 | } 57 | 58 | #issue-count { 59 | display: none; 60 | } 61 | 62 | .quote { 63 | border: none; 64 | margin: 20px 0 60px 0; 65 | } 66 | 67 | .quote p { 68 | font-style: italic; 69 | } 70 | 71 | .quote p:before { 72 | content: '“'; 73 | font-size: 50px; 74 | opacity: .15; 75 | position: absolute; 76 | top: -20px; 77 | left: 3px; 78 | } 79 | 80 | .quote p:after { 81 | content: '”'; 82 | font-size: 50px; 83 | opacity: .15; 84 | position: absolute; 85 | bottom: -42px; 86 | right: 3px; 87 | } 88 | 89 | .quote footer { 90 | position: absolute; 91 | bottom: -40px; 92 | right: 0; 93 | } 94 | 95 | .quote footer img { 96 | border-radius: 3px; 97 | } 98 | 99 | .quote footer a { 100 | margin-left: 5px; 101 | vertical-align: middle; 102 | } 103 | 104 | .speech-bubble { 105 | position: relative; 106 | padding: 10px; 107 | background: rgba(0, 0, 0, .04); 108 | border-radius: 5px; 109 | } 110 | 111 | .speech-bubble:after { 112 | content: ''; 113 | position: absolute; 114 | top: 100%; 115 | right: 30px; 116 | border: 13px solid transparent; 117 | border-top-color: rgba(0, 0, 0, .04); 118 | } 119 | 120 | .learn-bar > .learn { 121 | position: absolute; 122 | width: 272px; 123 | top: 8px; 124 | left: -300px; 125 | padding: 10px; 126 | border-radius: 5px; 127 | background-color: rgba(255, 255, 255, .6); 128 | transition-property: left; 129 | transition-duration: 500ms; 130 | } 131 | 132 | @media (min-width: 899px) { 133 | .learn-bar { 134 | width: auto; 135 | padding-left: 300px; 136 | } 137 | 138 | .learn-bar > .learn { 139 | left: 8px; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /resources/public/hooks.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hooks Demo 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /resources/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Fulcro Workspaces 5 | 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /resources/public/roots.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Multiple Roots Demo 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /resources/public/todo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | TodoMVC 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /shadow-cljs.edn: -------------------------------------------------------------------------------- 1 | {:deps {:aliases [:dev :test :workspaces]} 2 | :nrepl {:port 9000} 3 | :dev-http {9001 "resources/public"} 4 | :jvm-opts ["-Xmx2G"] 5 | :builds {:workspaces {:target nubank.workspaces.shadow-cljs.target 6 | :ns-regexp "-cards$" 7 | :output-dir "resources/public/js/workspaces" 8 | :asset-path "/js/workspaces" 9 | :compiler-options {:external-config {:fulcro {:html-source-annotations? true}}} 10 | :preloads [com.fulcrologic.fulcro.inspect.preload 11 | com.fulcrologic.fulcro.inspect.dom-picker-preload]} 12 | 13 | :todomvc {:target :browser 14 | :output-dir "resources/public/js/todomvc" 15 | :asset-path "/js/todomvc" 16 | :dev {:compiler-options {:external-config {:guardrails {}}}} 17 | :modules {:main {:entries [fulcro-todomvc.main]}} 18 | :devtools {:preloads [com.fulcrologic.devtools.chrome-preload]}} 19 | 20 | :test {:target :browser-test 21 | :test-dir "resources/public/js/test" 22 | :ns-regexp "-(test|spec)$" 23 | :compiler-options {:static-fns false 24 | :external-config {:guardrails {:throw? true :emit-spec? true}}} 25 | :js-options {:resolve {"react-dom" {:target :npm 26 | :require "react-dom/cjs/react-dom.production.min.js"} 27 | "react" {:target :npm 28 | :require "react/cjs/react.production.min.js"}}} 29 | :devtools {:http-port 9002 30 | :http-resource-root "public" 31 | :http-root "resources/public/js/test"}} 32 | 33 | :ci-tests {:target :karma 34 | :js-options {:js-provider :shadow} 35 | :compiler-options {:static-fns false ; required for mocking to work 36 | :external-config {:guardrails {:throw? true :emit-spec? true}}} 37 | :output-to "target/ci.js" 38 | :ns-regexp "-(test|spec)$"}}} 39 | -------------------------------------------------------------------------------- /src/clj-kondo/clj-kondo.exports/com.fulcrologic/fulcro/com/fulcrologic/fulcro/clj_kondo_hooks.clj: -------------------------------------------------------------------------------- 1 | (ns com.fulcrologic.fulcro.clj-kondo-hooks 2 | (:require [clj-kondo.hooks-api :as api])) 3 | 4 | (defn defmutation 5 | [{:keys [node]}] 6 | (let [args (rest (:children node)) 7 | mutation-name (first args) 8 | ?docstring (when (string? (api/sexpr (second args))) 9 | (second args)) 10 | args (if ?docstring 11 | (nnext args) 12 | (next args)) 13 | params (first args) 14 | handlers (rest args) 15 | handler-syms (map (comp first :children) handlers) 16 | bogus-usage (api/vector-node (vec handler-syms)) 17 | letfn-node (api/list-node 18 | (list 19 | (api/token-node 'letfn) 20 | (api/vector-node (vec handlers)) 21 | bogus-usage)) 22 | new-node (api/list-node 23 | (list 24 | (api/token-node 'defn) 25 | mutation-name 26 | params 27 | letfn-node))] 28 | (doseq [handler handlers] 29 | (let [hname (some-> handler :children first api/sexpr str) 30 | argv (some-> handler :children second)] 31 | (when-not (= 1 (count (api/sexpr argv))) 32 | (api/reg-finding! (merge 33 | (meta argv) 34 | {:message (format "defmutation handler '%s' should be a fn of 1 arg" hname) 35 | :type :clj-kondo.fulcro.defmutation/handler-arity}))))) 36 | {:node new-node})) 37 | 38 | (defn >defn 39 | [{:keys [node]}] 40 | (let [args (rest (:children node)) 41 | fn-name (first args) 42 | ?docstring (when (string? (api/sexpr (second args))) 43 | (second args)) 44 | args (if ?docstring 45 | (nnext args) 46 | (next args)) 47 | argv (first args) 48 | gspec (second args) 49 | body (nnext args) 50 | new-node (api/list-node 51 | (list* 52 | (api/token-node 'defn) 53 | fn-name 54 | argv 55 | gspec 56 | body))] 57 | (when (not= (count (api/sexpr argv)) 58 | (count (take-while #(not= '=> %) (api/sexpr gspec)))) 59 | (api/reg-finding! (merge (meta gspec) 60 | {:message "Guardrail spec does not match function signature" 61 | :type :clj-kondo.fulcro.>defn/signature-mismatch}))) 62 | {:node new-node})) -------------------------------------------------------------------------------- /src/clj-kondo/clj-kondo.exports/com.fulcrologic/fulcro/config.edn: -------------------------------------------------------------------------------- 1 | {:hooks {:analyze-call {com.fulcrologic.fulcro.mutations/defmutation com.fulcrologic.fulcro.clj-kondo-hooks/defmutation 2 | com.fulcrologic.guardrails.core/>defn com.fulcrologic.fulcro.clj-kondo-hooks/>defn}} 3 | :linters {:clj-kondo.fulcro.defmutation/handler-arity {:level :error} 4 | :clj-kondo.fulcro.>defn/signature-mismatch {:level :error}} 5 | :lint-as {com.fulcrologic.fulcro.algorithms.normalized-state/swap!-> clojure.core/-> 6 | com.fulcrologic.fulcro.components/defsc clojure.core/defn 7 | com.fulcrologic.fulcro.inspect.inspect-client/ido clojure.core/do 8 | com.fulcrologic.fulcro.inspect.inspect-client/ilet clojure.core/let 9 | com.fulcrologic.fulcro.mutations/declare-mutation clojure.core/def 10 | com.fulcrologic.fulcro.raw.components/defnc clojure.core/def 11 | com.fulcrologic.fulcro.routing.dynamic-routing/defrouter clojure.core/defn 12 | com.fulcrologic.fulcro.routing.legacy-ui-routers/defsc-router clojure.core/defn 13 | com.fulcrologic.fulcro.ui-state-machines/defstatemachine clojure.core/def 14 | com.fulcrologic.guardrails.core/>def clojure.core/def 15 | com.fulcrologic.guardrails.core/>defn clojure.core/defn 16 | com.fulcrologic.guardrails.core/>defn- clojure.core/defn- 17 | com.fulcrologic.rad.attributes/defattr clojure.core/def 18 | com.fulcrologic.rad.authorization/defauthenticator clojure.core/def 19 | com.fulcrologic.rad.form/defsc-form clojure.core/defn 20 | com.fulcrologic.rad.report/defsc-report clojure.core/defn}} 21 | -------------------------------------------------------------------------------- /src/dev/user.clj: -------------------------------------------------------------------------------- 1 | (ns user 2 | (:require 3 | [clojure.tools.namespace.repl :as tools-ns :refer [set-refresh-dirs]])) 4 | 5 | (set-refresh-dirs "src/main" "src/test" "src/dev" "src/todomvc") 6 | -------------------------------------------------------------------------------- /src/main/com/fulcrologic/fulcro/algorithms/lookup.cljc: -------------------------------------------------------------------------------- 1 | (ns com.fulcrologic.fulcro.algorithms.lookup 2 | "Fulcro is quite customizable, and all of the pluggable algorithms are stored on the app. This 3 | very easily leads to a desire to alias the long com.fulcrologic.fulcro.application namespace 4 | to something like `app` for easy access to keyword aliasing, but in Clojure this leads 5 | to circular references. This namespace exists simply to save typing and hassle with 6 | respect to that. It includes `app-algorithm` which can look up a plug-in algorithm on 7 | an app using a simple keyword without having to require the application ns." 8 | (:require 9 | [taoensso.timbre :as log])) 10 | 11 | (defn app-algorithm 12 | "Get the current value of a particular Fulcro plugin algorithm. These are set by default and can be overridden 13 | when you create your fulcro app. 14 | 15 | `app` - The application 16 | `k` - the algorithm to obtain. This can be a plain keyword or a symbol of the algorithm desired. 17 | 18 | Supported algorithms that can be obtained/overridden in Fulcro (check the source of app/fulcro-app if you suspect this is out 19 | of date, which is likely is): 20 | 21 | - `:global-eql-transform` - A `(fn [tx] tx')` that is applied to all outgoing requests (when using default `tx!`). 22 | Defaults to stripping things like `:ui/*` and form state config joins. 23 | - `:remote-error?` - A `(fn [result] boolean)` that defines what a remote error is. 24 | - `:global-error-action` - A `(fn [env] ...)` that is run on any remote error (as defined by `remote-error?`). 25 | - `:optimized-render!` - The concrete render algorithm for optimized renders (not root refreshes) 26 | - `:render!` - The top-level render function. Calls root render or optimized render by default. Renders on the calling thread. 27 | - `:schedule-render!` - The call that schedules a render. Defaults to using `js/requestAnimationFrame`. 28 | - `:default-result-action!` - The action used for remote results in all mutations that do not have a `result-action` section. 29 | - `:index-root!` - The algorithm that scans the current query from root an indexes all classes by their queries. 30 | - `:index-component!` - The algorithm that adds a component to indexes when it mounts. 31 | - `:drop-component!` - The algorithm that removes a component from indexes when it unmounts. 32 | - `:props-middleware` - Middleware that can modify `props` for all components. 33 | - `:render-middleware` - Middlware that wraps all `render` methods of `defsc` components. 34 | - `:before-render - A function `(fn [app RootClass])` that is called after a transaction completes, just BEFORE 35 | rendering. This function is allowed to affect the state atom to do things like compute dynamic derived state. Prefer 36 | this over an atom watch, since it will be called less frequently that an atom watch. This will be called EVEN IF 37 | Fulcro is running \"headless\". So, it can be thought of as `after-transaction`. 38 | 39 | Returns nil if the algorithm is currently undefined. 40 | " 41 | [{:com.fulcrologic.fulcro.application/keys [algorithms] :as app} k] 42 | (when-let [nm (when (or (string? k) (keyword? k) (symbol? k)) 43 | (keyword "com.fulcrologic.fulcro.algorithm" (name k)))] 44 | (get-in app [:com.fulcrologic.fulcro.application/algorithms nm] nil))) 45 | -------------------------------------------------------------------------------- /src/main/com/fulcrologic/fulcro/algorithms/react_interop.cljc: -------------------------------------------------------------------------------- 1 | (ns com.fulcrologic.fulcro.algorithms.react-interop 2 | (:require 3 | [com.fulcrologic.fulcro.components :as comp] 4 | #?(:cljs [com.fulcrologic.fulcro.dom :as dom] 5 | :clj [com.fulcrologic.fulcro.dom-server :as dom]) 6 | [taoensso.timbre :as log])) 7 | 8 | (defn react-factory 9 | "Returns a factory for raw JS React classes. 10 | 11 | ``` 12 | (def ui-thing (react-factory SomeReactLibComponent)) 13 | 14 | ... 15 | (defsc X [_ _] 16 | (ui-thing {:value 1})) 17 | ``` 18 | 19 | The returned function will accept CLJS maps as props (not optional) and then any number of children. The CLJS props 20 | will be converted to js for interop. You may pass js props as an optimization." 21 | [js-component-class] 22 | (fn [props & children] 23 | #?(:cljs 24 | (apply dom/create-element 25 | js-component-class 26 | (dom/convert-props props) 27 | children)))) 28 | 29 | (defn react-input-factory 30 | "Returns a factory for raw JS React class that acts like an input. Use this on custom raw React controls are 31 | controlled via :value to make them behave properly with Fulcro. 32 | 33 | ``` 34 | (def ui-thing (react-input-factory SomeInputComponent)) 35 | 36 | ... 37 | (defsc X [_ _] 38 | (ui-thing {:value 1})) 39 | ``` 40 | 41 | The returned function will accept CLJS maps as props (not optional) and then any number of children. The CLJS props 42 | will be converted to js for interop. You may pass js props as an optimization." 43 | [js-component-class] 44 | #?(:cljs 45 | (let [factory (dom/wrap-form-element js-component-class)] 46 | (fn [props & children] 47 | (apply factory (clj->js props) children))) 48 | :default 49 | (fn [props & children]))) 50 | 51 | (defn hoc-wrapper-factory 52 | "Creates a React factory `(fn [parent fulcro-props & children])` for a component that has had an HOC applied, 53 | and passes Fulcro's parent/props through to 'fulcro_hoc$parent' and 'fulcro_hoc_childprops' in the js props. 54 | 55 | See hoc-factory, which is more likely what you want, as it further wraps the parent context for proper interop." 56 | [component-class] 57 | (fn [this props & children] 58 | (when-not (comp/component? this) 59 | (log/error "The first argument to an HOC factory MUST be the parent component instance. See https://book.fulcrologic.com/#err-interop-1st-arg-not-parent")) 60 | #?(:cljs 61 | (apply dom/create-element 62 | component-class 63 | #js {"fulcro_hoc$parent" this 64 | "fulcro_hoc$childprops" props} 65 | children)))) 66 | 67 | (defn hoc-factory 68 | "Returns a (fn [parent-component props & children] ...) that will render the target-fulcro-class, but as 69 | wrapped by the `hoc` function. 70 | 71 | Use this when you have a JS React pattern that tells you: 72 | 73 | ``` 74 | var WrappedComponent = injectCrap(Component); 75 | ``` 76 | 77 | where `injectCrap` is the `hoc` parameter to this function. 78 | 79 | Any injected data will appear as `:injected-props` (a js map) in the computed parameter of the target Fulcro component. 80 | 81 | You can this use the function returned from `hoc-factory` as a normal component factory in fulcro. 82 | " 83 | [target-fulcro-class hoc] 84 | (when-not (comp/component-class? target-fulcro-class) 85 | (log/error "hoc-factory MUST be used with a Fulcro Class. See https://book.fulcrologic.com/#err-interop-not-fulcro-class")) 86 | (let [target-factory (comp/computed-factory target-fulcro-class) 87 | target-factory-interop (fn [js-props] 88 | (let [parent (comp/isoget js-props "fulcro_hoc$parent") 89 | fulcro-props (comp/isoget js-props "fulcro_hoc$childprops")] 90 | (comp/with-parent-context parent 91 | (target-factory fulcro-props {:injected-props js-props})))) 92 | factory (let [WrappedComponent (hoc target-factory-interop)] 93 | (hoc-wrapper-factory WrappedComponent))] 94 | factory)) 95 | -------------------------------------------------------------------------------- /src/main/com/fulcrologic/fulcro/algorithms/scheduling.cljc: -------------------------------------------------------------------------------- 1 | (ns com.fulcrologic.fulcro.algorithms.scheduling 2 | "Algorithms for delaying some action by a particular amount of time." 3 | (:require 4 | [com.fulcrologic.guardrails.core :refer [>fdef =>]] 5 | [clojure.core.async :as async] 6 | [taoensso.timbre :as log])) 7 | 8 | #?(:cljs 9 | (defn defer 10 | "Schedule f to run in `tm` ms." 11 | [f tm] 12 | (js/setTimeout f tm)) 13 | :clj 14 | (do 15 | (defonce timeout-queue (async/chan 100)) 16 | (defonce scheduling-timeout-loop 17 | (async/go-loop [] 18 | (let [{:keys [active f]} (async/! timeout-queue {:active active 33 | :f f})) 34 | cancel)))) 35 | 36 | (defn schedule! 37 | "Schedule the processing of a specific action in the runtime atom. This is a no-op if the item is already scheduled. 38 | When the timeout arrives it runs the given action and sets the given flag back to false. 39 | 40 | - `scheduled-key` - The runtime flag that tracks scheduling for the processing. 41 | - `action` - The function to run when the scheduled time comes. 42 | - `tm` - Number of ms to delay (default 0)." 43 | ([app scheduled-key action tm] 44 | [:com.fulcrologic.fulcro.application/app keyword? fn? int? => any?] 45 | (let [{:com.fulcrologic.fulcro.application/keys [runtime-atom]} app] 46 | (when-not (get @runtime-atom scheduled-key) 47 | (swap! runtime-atom assoc scheduled-key true) 48 | (defer (fn [] 49 | (swap! runtime-atom assoc scheduled-key false) 50 | (action app)) tm)))) 51 | ([app scheduled-key action] 52 | [:com.fulcrologic.fulcro.application/app keyword? fn? => any?] 53 | (schedule! app scheduled-key action 0))) 54 | 55 | (let [raf #?(:clj #(defer % 16) 56 | :cljs (if (exists? js/requestAnimationFrame) 57 | js/requestAnimationFrame 58 | #(defer % 16)))] 59 | (defn schedule-animation! 60 | "Schedule the processing of a specific action in the runtime atom on the next animation frame. 61 | 62 | - `scheduled-key` - The runtime flag that tracks scheduling for the processing. 63 | - `action` - The function to run when the scheduled time comes." 64 | ([app scheduled-key action] 65 | [:com.fulcrologic.fulcro.application/app keyword? fn? => any?] 66 | #?(:clj (action) 67 | :cljs (let [{:com.fulcrologic.fulcro.application/keys [runtime-atom]} app] 68 | (when-not (get @runtime-atom scheduled-key) 69 | (swap! runtime-atom assoc scheduled-key true) 70 | (let [f (fn [] 71 | (swap! runtime-atom assoc scheduled-key false) 72 | (action))] 73 | (raf f)))))))) 74 | -------------------------------------------------------------------------------- /src/main/com/fulcrologic/fulcro/algorithms/server_render.cljc: -------------------------------------------------------------------------------- 1 | (ns com.fulcrologic.fulcro.algorithms.server-render 2 | (:require 3 | [com.fulcrologic.fulcro.algorithms.transit :as transit] 4 | [com.fulcrologic.fulcro.algorithms.merge :as merge] 5 | [com.fulcrologic.fulcro.algorithms.normalize :refer [tree->db]] 6 | [com.fulcrologic.fulcro.algorithms.do-not-use :refer [base64-encode base64-decode]])) 7 | 8 | (defn initial-state->script-tag 9 | "Returns a *string* containing an HTML script tag that that sets js/window.INITIAL_APP_STATE to a transit-encoded string version of initial-state. 10 | 11 | `opts` is a map to be passed to the transit writer. 12 | `string-transform` should be a function with 1 argument. The stringified app-state is passed to it. 13 | This is the place to perform additional string replacement operations to escape special characters, 14 | as in the case of encoded polylines." 15 | ([initial-state] (initial-state->script-tag initial-state {} identity)) 16 | ([initial-state opts] (initial-state->script-tag initial-state opts identity)) 17 | ([initial-state opts string-transform] 18 | (let [state-string (-> (transit/transit-clj->str initial-state opts) 19 | (string-transform) 20 | (base64-encode)) 21 | assignment (str "window.INITIAL_APP_STATE = '" state-string "'")] 22 | (str 23 | "\n")))) 26 | 27 | #?(:cljs 28 | (defn get-SSR-initial-state 29 | "Obtain the value of the INITIAL_APP_STATE set from server-side rendering. Use initial-state->script-tag on the server to embed the state." 30 | ([] (get-SSR-initial-state {})) 31 | ([opts] 32 | (when-let [state-string (some-> js/window .-INITIAL_APP_STATE base64-decode)] 33 | (transit/transit-str->clj state-string opts))))) 34 | 35 | (defn build-initial-state 36 | "This function normalizes the given state-tree using the root-component's query into standard client db format, 37 | it then walks the query and adds any missing data from union branches that are not the 'default' branch 38 | on the union itself. E.g. A union with initial state can only point to one thing, but you need the other branches 39 | in the normalized application database. Assumes all components (except possibly root-class) that need initial state 40 | use `:initial-state`. 41 | 42 | Useful for building a pre-populated db for server-side rendering. 43 | 44 | Returns a normalized client db with all union alternates initialized to their InitialAppState." 45 | [state-tree root-class] 46 | (let [base-state (tree->db root-class state-tree true (merge/pre-merge-transform {})) 47 | base-state (merge/merge-alternate-union-elements base-state root-class)] 48 | base-state)) 49 | 50 | -------------------------------------------------------------------------------- /src/main/com/fulcrologic/fulcro/algorithms/timbre_support.cljs: -------------------------------------------------------------------------------- 1 | (ns com.fulcrologic.fulcro.algorithms.timbre-support 2 | " 3 | Logging helpers to make js console logging more readable. The recommended use of these functions is as follows: 4 | 5 | - Make sure you're using Binaryage devtools (on classpath. shadow-cljs will auto-add it when detected). 6 | - IMPORTANT: Enable custom formatters in console settings for Chrome. This will print cljs data as cljs (instead of raw js). 7 | - Make a development preload cljs file, and tell shadow-cljs to preload it. 8 | - In the preload file, add something like this: 9 | 10 | ``` 11 | (ns app.development-preload 12 | (:require 13 | [taoensso.timbre :as log] 14 | [com.fulcrologic.fulcro.algorithms.timbre-support :refer [console-appender prefix-output-fn])) 15 | 16 | (log/set-level! :debug) 17 | (log/merge-config! {:output-fn prefix-output-fn 18 | :appenders {:console (console-appender)}}) 19 | ``` 20 | 21 | and you'll get much more readable error messages in the js console. 22 | 23 | NOTE: when logging errors, be sure to log the exception first. This is documented in timbre, but easy to miss: 24 | 25 | ``` 26 | (try 27 | ... 28 | (catch :default ex 29 | (log/error ex ...)) 30 | ``` 31 | 32 | See the development_preload.cljs and shadow-cljs.edn files in the latest Fulcro 3 template for an example. 33 | " 34 | (:require 35 | [clojure.string :as str])) 36 | 37 | ;; Taken mostly from timbre itself. Modified to output better results from ex-info exceptions (e.g. to improve expound 38 | ;; experience) 39 | (defn console-appender 40 | "Returns a js/console appender for ClojureScript. This appender uses the normal output-fn to generate the main 41 | message, but it also does raw output of the original logging args so that devtools can format data structures. 42 | 43 | Furthermore, if it detects an ExceptionInfo it will print the `ex-message` *after* so that you can see the real 44 | message of the exception last in the console. This is particularly handy when using specs and expound with 45 | spec instrumentation. 46 | 47 | For accurate line numbers in Chrome, add these Blackbox[1] patterns: 48 | `/taoensso/timbre/appenders/core\\.js$` 49 | `/taoensso/timbre\\.js$` 50 | `/cljs/core\\.js$` 51 | 52 | [1] Ref. https://goo.gl/ZejSvR" 53 | [& [opts]] 54 | {:enabled? true 55 | :async? false 56 | :min-level nil 57 | :rate-limit nil 58 | :output-fn :inherit 59 | :fn (if (exists? js/console) 60 | (let [;; Don't cache this; some libs dynamically replace js/console 61 | level->logger 62 | (fn [level] 63 | (or 64 | (case level 65 | :trace js/console.trace 66 | :debug js/console.debug 67 | :info js/console.info 68 | :warn js/console.warn 69 | :error js/console.error 70 | :fatal js/console.error 71 | :report js/console.info) 72 | js/console.log))] 73 | 74 | (fn [{:keys [level vargs ?err output-fn] :as data}] 75 | (when-let [logger (level->logger level)] 76 | (let [output (when output-fn (output-fn (assoc data :msg_ "" :?err nil))) 77 | args (if-let [err ?err] 78 | (cons output (cons err vargs)) 79 | (cons output vargs))] 80 | (.apply logger js/console (into-array args)) 81 | (when (instance? ExceptionInfo ?err) 82 | (js/console.log (ex-message ?err))))))) 83 | (fn [data] nil))}) 84 | 85 | (defn prefix-output-fn 86 | "Mostly taken from timbre, but just formats message prefix as output (e.g. only location/line/level). Use with the 87 | console appender from this namespace to get better logging output in cljs." 88 | ([data] (prefix-output-fn nil data)) 89 | ([opts data] ; For partials 90 | (let [{:keys [level ?ns-str ?file ?line]} data] 91 | (str (str/upper-case (name level)) " " "[" (or ?ns-str ?file "?") ":" (or ?line "?") "] - ")))) 92 | 93 | 94 | -------------------------------------------------------------------------------- /src/main/com/fulcrologic/fulcro/algorithms/tx-processing-readme.adoc: -------------------------------------------------------------------------------- 1 | == Optimizing Sends 2 | 3 | The internals of the tx processing splits the original transactions so that there is one tx-node per element of the top-level transaction. 4 | Loads use an `internal-load` mutation, so they are always a single top-level tx. 5 | 6 | To Do: 7 | 8 | - There should be a function that takes the nodes with the same ID that are queued and re-combines them. 9 | This is the current combine-sends, but the logic needs to not do reordering or even return what to send. 10 | - The next step of processing the send queue should be optional write reordering 11 | - The next step should then be potentially creating a multi-send send node so that the entire queue can be sent on one network request. 12 | 13 | So, desired send processing: 14 | 15 | 1. restore transaction semantics: fn of send-queue -> send-queue. 16 | Combines send nodes with a common tx ID into a single send node so that as much of the original tx will be sent as a unit as is possible. 17 | 2. Reorder the queue: Pluggable and optional. 18 | Put writes first, enable customization like "tx priorities" 19 | 3. Merge the queue (optional, requires enabling on client and server): For a given remote it is possible for us to encode a "multi-send", where the send nodes from (1/2) are combined into a data structure that allows the lower-level networking to send the entire queue in one network round-trip. 20 | Something like a vector of maps? 21 | `[{::id id ::tx tx} {::id id2 ::tx tx}]`, where the server returns `{id result id2 result ...}`. 22 | 23 | [source,clojure] 24 | ----- 25 | (defmutation f [_] 26 | (action ..opt...) 27 | (result-action [env] ...) 28 | (rmt1 ...)) 29 | 30 | (defmutation g [_] 31 | (action [env] 32 | (transact! this [(i)])) 33 | (result-action [env] ...) ; has a default 34 | (rmt2 [env] 35 | (-> env 36 | (m/with-target env [:x]) 37 | (m/returning env Person)))) 38 | 39 | ;; could execute either true/false on optimistic?: 40 | (transact! this [(f) (g)] {:optimistic? true}) 41 | ;; f + g opt + f on rmt1 + g one rmt2 (all at once) 42 | 43 | ;; opt false is same as ptransact! 44 | (ptransact! this [(f) (g)]) 45 | ;; f opt + f net, then on net result: g opt g net 46 | ----- 47 | 48 | [ditaa] 49 | ----- 50 | | 51 | | Into submission queue 52 | v 53 | +-------+ +-------+ 54 | sub q |(f) (g)| | | ... 55 | | UUID | | | 56 | +--+----+ +---+---+ 57 | | | 58 | +----+-----+ 59 | | (user releases thread) 60 | | Activation (dispatch of f and g, which returns maps like {:action ...}) 61 | v 62 | +-----------------------+ 63 | | +-------+ +-------+ +<---------------+ 64 | active Q | | (f) | | (g) | ... (tx nodes) | RUN A Q STEP 65 | | | RA | | | | | Run through active queue and process 66 | | +-------+ +-------+ +----------------+ Also dispatches results to result-action 67 | +-----------------------+ 68 | ^ | | 69 | | +----+-----+ 70 | net | | 71 | result +--+ | (possible re-combining of tx) 72 | | v 73 | +--+----+ +-------+ 74 | Send Q Rmt1 |(f) | | | ... 75 | +-------+ +-------+ 76 | 77 | +-------+ +-------+ 78 | Send Q Rmt2 | (g)| | | ... 79 | +-------+ +-------+ 80 | 81 | ----- -------------------------------------------------------------------------------- /src/main/com/fulcrologic/fulcro/algorithms/tx_processing_debug.cljc: -------------------------------------------------------------------------------- 1 | (ns com.fulcrologic.fulcro.algorithms.tx-processing-debug 2 | "Helper functions for debugging tx processing. Uses pprint, which adds 3 | a lot to build size, so it is in a separate ns to keep it out of prod builds." 4 | (:require 5 | [clojure.string :as str] 6 | [clojure.pprint :refer [pprint]])) 7 | 8 | (defn tx-status! 9 | "Debugging function. Shows the current transaction queues with a summary of their content." 10 | [location {:com.fulcrologic.fulcro.application/keys [runtime-atom] :as app}] 11 | (let [{:com.fulcrologic.fulcro.algorithms.tx-processing/keys [submission-queue active-queue send-queues]} @runtime-atom 12 | strks (fn [n & ks] 13 | (str/join "\n" (map (fn [k] (str k " = " (with-out-str (pprint (get n k))))) ks))) 14 | prtxnode (fn [n] 15 | (println (strks n :com.fulcrologic.fulcro.algorithms.tx-processing/id :com.fulcrologic.fulcro.algorithms.tx-processing/tx)) 16 | (doseq [{:com.fulcrologic.fulcro.algorithms.tx-processing/keys [idx results dispatch] :as ele} 17 | (:com.fulcrologic.fulcro.algorithms.tx-processing/elements n)] 18 | (println " Element " idx) 19 | (println " " (strks ele :com.fulcrologic.fulcro.algorithms.tx-processing/started? 20 | :com.fulcrologic.fulcro.algorithms.tx-processing/complete? 21 | :com.fulcrologic.fulcro.algorithms.tx-processing/original-ast-node)) 22 | (println " Dispatch: " (with-out-str (pprint dispatch))) 23 | (println " Results: " (with-out-str (pprint results))))) 24 | prsend (fn [s] 25 | (println "NODE:") 26 | (println " " (strks s :com.fulcrologic.fulcro.algorithms.tx-processing/ast :com.fulcrologic.fulcro.algorithms.tx-processing/active?)))] 27 | (println location) 28 | (println "================================================================================") 29 | (println "Submission Queue:") 30 | (doseq [n submission-queue] 31 | (prtxnode n)) 32 | (println "======") 33 | (println "Active Queue:") 34 | (doseq [n active-queue] 35 | (prtxnode n)) 36 | (println "======") 37 | (println "Send Queues:") 38 | (doseq [k (keys send-queues)] 39 | (println k " Send Queue:") 40 | (doseq [n (get send-queues k)] 41 | (prsend n))) 42 | (println "================================================================================"))) 43 | -------------------------------------------------------------------------------- /src/main/com/fulcrologic/fulcro/dom/events.cljc: -------------------------------------------------------------------------------- 1 | (ns com.fulcrologic.fulcro.dom.events 2 | "Utility functions for working with low-level synthetic js events on the DOM") 3 | 4 | (defn stop-propagation! 5 | "Calls .stopPropagation on the given event. Safe to use in CLJC files." 6 | [evt] #?(:cljs (.stopPropagation ^js evt))) 7 | 8 | (defn prevent-default! 9 | "Calls .preventDefault on the given event. Safe to use in CLJC files." 10 | [evt] #?(:cljs (.preventDefault ^js evt))) 11 | 12 | (defn target-value 13 | "Returns the event #js evt.target.value. Safe to use in CLJC." 14 | [evt] 15 | #?(:cljs (.. evt -target -value))) 16 | 17 | (defn is-key? 18 | "Is the given key code on the given event?" 19 | #?(:cljs {:tag boolean}) 20 | [code evt] (= code (.-keyCode evt))) 21 | 22 | (defn enter-key? "Returns true if the event has the keyCode of the function name." #?(:cljs {:tag boolean}) [evt] (is-key? 13 evt)) 23 | (defn escape-key? "Returns true if the event has the keyCode of the function name." #?(:cljs {:tag boolean}) [evt] (is-key? 27 evt)) 24 | (defn left-arrow? "Returns true if the event has the keyCode of the function name." #?(:cljs {:tag boolean}) [evt] (is-key? 37 evt)) 25 | (defn right-arrow? "Returns true if the event has the keyCode of the function name." #?(:cljs {:tag boolean}) [evt] (is-key? 39 evt)) 26 | (defn up-arrow? "Returns true if the event has the keyCode of the function name." #?(:cljs {:tag boolean}) [evt] (is-key? 38 evt)) 27 | (defn down-arrow? "Returns true if the event has the keyCode of the function name." #?(:cljs {:tag boolean}) [evt] (is-key? 40 evt)) 28 | (defn page-up? "Returns true if the event has the keyCode of the function name." #?(:cljs {:tag boolean}) [evt] (is-key? 33 evt)) 29 | (defn page-down? "Returns true if the event has the keyCode of the function name." #?(:cljs {:tag boolean}) [evt] (is-key? 34 evt)) 30 | (defn enter? "Returns true if the event has the keyCode of the function name." #?(:cljs {:tag boolean}) [evt] (is-key? 13 evt)) 31 | (defn escape? "Returns true if the event has the keyCode of the function name." #?(:cljs {:tag boolean}) [evt] (is-key? 27 evt)) 32 | (defn delete? "Returns true if the event has the keyCode of the function name." #?(:cljs {:tag boolean}) [evt] (is-key? 46 evt)) 33 | (defn tab? "Returns true if the event has the keyCode of the function name." #?(:cljs {:tag boolean}) [evt] (is-key? 9 evt)) 34 | (defn end? "Returns true if the event has the keyCode of the function name." #?(:cljs {:tag boolean}) [evt] (is-key? 35 evt)) 35 | (defn home? "Returns true if the event has the keyCode of the function name." #?(:cljs {:tag boolean}) [evt] (is-key? 36 evt)) 36 | (defn alt? "Returns true if the event has the keyCode of the function name." #?(:cljs {:tag boolean}) [evt] (is-key? 18 evt)) 37 | (defn ctrl? "Returns true if the event has the keyCode of the function name." #?(:cljs {:tag boolean}) [evt] (is-key? 17 evt)) 38 | (defn shift? "Returns true if the event has the keyCode of the function name." #?(:cljs {:tag boolean}) [evt] (is-key? 16 evt)) 39 | (defn F1? "Returns true if the event has the keyCode of the function name." #?(:cljs {:tag boolean}) [evt] (is-key? 112 evt)) 40 | (defn F2? "Returns true if the event has the keyCode of the function name." #?(:cljs {:tag boolean}) [evt] (is-key? 113 evt)) 41 | (defn F3? "Returns true if the event has the keyCode of the function name." #?(:cljs {:tag boolean}) [evt] (is-key? 114 evt)) 42 | (defn F4? "Returns true if the event has the keyCode of the function name." #?(:cljs {:tag boolean}) [evt] (is-key? 115 evt)) 43 | (defn F5? "Returns true if the event has the keyCode of the function name." #?(:cljs {:tag boolean}) [evt] (is-key? 116 evt)) 44 | (defn F6? "Returns true if the event has the keyCode of the function name." #?(:cljs {:tag boolean}) [evt] (is-key? 117 evt)) 45 | (defn F7? "Returns true if the event has the keyCode of the function name." #?(:cljs {:tag boolean}) [evt] (is-key? 118 evt)) 46 | (defn F8? "Returns true if the event has the keyCode of the function name." #?(:cljs {:tag boolean}) [evt] (is-key? 119 evt)) 47 | (defn F9? "Returns true if the event has the keyCode of the function name." #?(:cljs {:tag boolean}) [evt] (is-key? 120 evt)) 48 | (defn F10? "Returns true if the event has the keyCode of the function name." #?(:cljs {:tag boolean}) [evt] (is-key? 121 evt)) 49 | (defn F11? "Returns true if the event has the keyCode of the function name." #?(:cljs {:tag boolean}) [evt] (is-key? 122 evt)) 50 | (defn F12? "Returns true if the event has the keyCode of the function name." #?(:cljs {:tag boolean}) [evt] (is-key? 123 evt)) 51 | -------------------------------------------------------------------------------- /src/main/com/fulcrologic/fulcro/inspect/devtool_api.cljc: -------------------------------------------------------------------------------- 1 | (ns com.fulcrologic.fulcro.inspect.devtool-api 2 | "These are declarations of the remote mutations that are callable on the Fulcro Inspect Dev tool. Internal use. 3 | 4 | They are declared in the Fulcro project so the internals can be connected to the Inspect Devtool without having 5 | the dev tool be a release build requirement." 6 | #?(:cljs (:require-macros com.fulcrologic.fulcro.inspect.devtool-api)) 7 | (:require 8 | [com.fulcrologic.fulcro.mutations :as m])) 9 | 10 | #?(:clj 11 | (defmacro remote-mutations [& syms] 12 | (let [declarations (mapv 13 | (fn [sym] 14 | `(m/defmutation ~sym [_#] 15 | (~'devtool-remote [_env#] true))) 16 | syms)] 17 | `(do 18 | ~@declarations)))) 19 | 20 | (com.fulcrologic.fulcro.inspect.devtool-api/remote-mutations 21 | app-started 22 | db-changed 23 | send-started 24 | send-finished 25 | send-failed 26 | focus-target 27 | optimistic-action 28 | statechart-event) 29 | -------------------------------------------------------------------------------- /src/main/com/fulcrologic/fulcro/inspect/diff.cljc: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc com.fulcrologic.fulcro.inspect.diff 2 | "Internal algorithms for sending db diffs to Inspect tool.") 3 | 4 | (defn updates [a b] 5 | (reduce 6 | (fn [adds [k v]] 7 | (let [va (get a k :fulcro.inspect.lib.diff/unset)] 8 | (if (= v va) 9 | adds 10 | (if (and (map? v) (map? va)) 11 | (assoc adds k (updates va v)) 12 | (assoc adds k v))))) 13 | {} 14 | b)) 15 | 16 | (defn removals [a b] 17 | (reduce 18 | (fn [rems [k v]] 19 | (if-let [[_ vb] (find b k)] 20 | (if (and (map? v) (map? vb) (not= v vb)) 21 | (let [childs (removals v vb)] 22 | (if (seq childs) 23 | (conj rems {k childs}) 24 | rems)) 25 | rems) 26 | (conj rems (cond-> k (map? k) (assoc :fulcro.inspect.lib.diff/key? true))))) 27 | [] 28 | a)) 29 | 30 | (defn diff [a b] 31 | {:fulcro.inspect.lib.diff/updates (updates a b) 32 | :fulcro.inspect.lib.diff/removals (removals a b)}) 33 | 34 | (defn deep-merge [x y] 35 | (if (and (map? x) (map? y)) 36 | (merge-with deep-merge x y) 37 | y)) 38 | 39 | (defn patch-updates [x {:fulcro.inspect.lib.diff/keys [updates]}] 40 | (merge-with deep-merge x updates)) 41 | 42 | (defn patch-removals [x {:fulcro.inspect.lib.diff/keys [removals]}] 43 | (reduce 44 | (fn [final rem] 45 | (cond 46 | (:fulcro.inspect.lib.diff/key? rem) 47 | (dissoc final (dissoc rem :fulcro.inspect.lib.diff/key?)) 48 | 49 | (map? rem) 50 | (let [[k v] (first rem)] 51 | (update final k #(patch-removals % {:fulcro.inspect.lib.diff/removals v}))) 52 | 53 | :else 54 | (dissoc final rem))) 55 | x 56 | removals)) 57 | 58 | (defn patch [x diff] 59 | (-> x 60 | (patch-updates diff) 61 | (patch-removals diff))) 62 | -------------------------------------------------------------------------------- /src/main/com/fulcrologic/fulcro/inspect/dom_picker_preload.cljs: -------------------------------------------------------------------------------- 1 | (ns com.fulcrologic.fulcro.inspect.dom-picker-preload 2 | (:require 3 | [com.fulcrologic.fulcro.inspect.element-picker :as ele])) 4 | 5 | (ele/install!) 6 | -------------------------------------------------------------------------------- /src/main/com/fulcrologic/fulcro/inspect/preload.cljs: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc com.fulcrologic.fulcro.inspect.preload 2 | "Namespace to use in your compiler preload in order to enable inspect support during development." 3 | (:require 4 | [taoensso.timbre :as log])) 5 | 6 | (log/error "Inspect NOT installed. This version of Fulcro requires you use fulcro inspect as a dev-time dependency, and explicitly call (fulcro.inspect.tool/add-fulcro-inspect! app) on your app,,") 7 | (log/error "NOTE: You will need to also add the preload: com.fulcrologic.devtools.chrome-preload to use Fulcro Inspect Chrome") 8 | -------------------------------------------------------------------------------- /src/main/com/fulcrologic/fulcro/inspect/tools.cljc: -------------------------------------------------------------------------------- 1 | (ns com.fulcrologic.fulcro.inspect.tools) 2 | 3 | (defn- rt [app] (:com.fulcrologic.fulcro.application/runtime-atom app)) 4 | (defn registered-tools [app] (get @(rt app) ::tools)) 5 | (defn register-tool! 6 | "Add a tool to Fulcro. 7 | 8 | `tool` is a `(fn [app event])`, where the event is a map that describes some internal operation 9 | of interest. The application layer or Fulcro itself can send such events, which are meant for external 10 | tooling such as Fulcro Inspect. " 11 | [app tool] (swap! (rt app) update ::tools (fnil conj []) tool)) 12 | 13 | (defn notify! 14 | "Notify all registered tools that some event of interest has occurred. These notifications are meant for tools 15 | like Fulcro Inspect, but your application layer or libraries can send additional events that can be consumed by 16 | any tooling you choose to define. 17 | 18 | event-name can be a string/keyword/symbol. It will be associated onto the event you receive as `:type`. The 19 | data MUST be a map, and this function will also automatically add the Fulcro application's UUID 20 | under the key :com.fulcrologic.fulcro.application/id and the application's description as 21 | :com.fulcrologic.fulcro.application/label" 22 | [app event-name data] 23 | (assert (map? data) "data is a map") 24 | (let [id (:com.fulcrologic.fulcro.application/id app) 25 | event (assoc data 26 | :type event-name 27 | :com.fulcrologic.fulcro.application/id id 28 | :com.fulcrologic.fulcro.application/label (or (:com.fulcrologic.fulcro.application/label app) (str id)))] 29 | (doseq [t (registered-tools app)] 30 | (t app event)))) 31 | -------------------------------------------------------------------------------- /src/main/com/fulcrologic/fulcro/inspect/transit.cljs: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc com.fulcrologic.fulcro.inspect.transit 2 | (:require [cognitect.transit :as t] 3 | [com.cognitect.transit.types :as ty] 4 | [com.fulcrologic.fulcro.algorithms.transit :as ft] 5 | [taoensso.timbre :as log])) 6 | 7 | (deftype ErrorHandler [] 8 | Object 9 | (tag [this v] "js-error") 10 | (rep [this v] [(ex-message v) (ex-data v)]) 11 | (stringRep [this v] (ex-message v))) 12 | 13 | (deftype DefaultHandler [] 14 | Object 15 | (tag [this v] "unknown") 16 | (rep [this v] (try 17 | (str v) 18 | (catch :default e 19 | (when goog.DEBUG 20 | (log/warn "Transit was unable to encode a value. See https://book.fulcrologic.com/#warn-transit-encode-failed")) 21 | "UNENCODED VALUE")))) 22 | 23 | (defn write-handlers [] 24 | (merge {cljs.core/ExceptionInfo (ErrorHandler.) 25 | "default" (DefaultHandler.)} 26 | (ft/write-handlers))) 27 | 28 | (defn read-handlers [] 29 | (merge 30 | {"js-error" (fn [[msg data]] (ex-info msg data))} 31 | (ft/read-handlers))) 32 | 33 | (defn read [str] 34 | (let [reader (ft/reader {:handlers (read-handlers)})] 35 | (t/read reader str))) 36 | 37 | (defn write [x] 38 | (let [writer (ft/writer {:handlers (write-handlers)})] 39 | (t/write writer x))) 40 | 41 | (extend-type ty/UUID IUUID) 42 | -------------------------------------------------------------------------------- /src/main/com/fulcrologic/fulcro/inspect/websocket_preload.cljs: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc com.fulcrologic.fulcro.inspect.websocket-preload 2 | (:require 3 | [taoensso.timbre :as log])) 4 | 5 | (log/error "Inspect NOT installed. This version of Fulcro requires you use fulcro inspect as a dev-time dependency, and explicitly call (fulcro.inspect.tool/add-fulcro-inspect! app) on your app,,") 6 | (log/error "NOTE: You will need to also add the preload: com.fulcrologic.devtools.electron-preload to use Fulcro Inspect Electron") 7 | -------------------------------------------------------------------------------- /src/main/com/fulcrologic/fulcro/networking/file_upload.clj: -------------------------------------------------------------------------------- 1 | (ns com.fulcrologic.fulcro.networking.file-upload 2 | (:require [edn-query-language.core :as eql] 3 | [taoensso.timbre :as log] 4 | [taoensso.encore :as enc] 5 | [clojure.string :as str] 6 | [clojure.tools.reader.edn :as edn] 7 | [com.fulcrologic.fulcro.algorithms.transit :as transit])) 8 | 9 | (defn- upload-transaction 10 | [req] 11 | (or 12 | (get-in req [:params :upload-transaction]) 13 | (get-in req [:params "upload-transaction"]))) 14 | 15 | (defn- upload-files 16 | [req] 17 | (when-let [files (or 18 | (get-in req [:multipart-params "files"]) 19 | (get-in req [:multipart-params :files]))] 20 | (if (map? files) 21 | [files] 22 | files))) 23 | 24 | (defn- transaction-with-uploads? 25 | [req] 26 | (string? (upload-transaction req))) 27 | 28 | (defn- attach-uploads-to-txn 29 | [txn files] 30 | (try 31 | (let [ast (eql/query->ast txn) 32 | mutation->files (reduce (fn [result {:keys [filename] :as file}] 33 | (enc/if-let [[mutation-name filename] (some-> filename (str/split #"[%]")) 34 | mutation-sym (some-> mutation-name (edn/read-string))] 35 | (update result mutation-sym (fnil conj []) (assoc file :filename filename)) 36 | (do 37 | (log/error "Unable to associate a file with a mutation" file "See https://book.fulcrologic.com/#err-fu-cant-assoc-file") 38 | result))) 39 | {} 40 | files) 41 | new-ast (update ast :children 42 | #(mapv 43 | (fn [{:keys [dispatch-key] :as n}] 44 | (if (contains? mutation->files dispatch-key) 45 | (assoc-in n [:params ::files] (mutation->files dispatch-key)) 46 | n)) 47 | %))] 48 | (eql/ast->query new-ast)) 49 | (catch Exception e 50 | (log/error e "Unable to attach uploads to the transaction. See https://book.fulcrologic.com/#err-fu-cant-attach-uploads") 51 | txn))) 52 | 53 | (defn wrap-mutation-file-uploads 54 | "Middleware that enables the server to handle mutations that have attached file uploads to their parameters. 55 | This middleware must be composed after Ring middleware for multipart (*required*), but 56 | before the API handling. 57 | 58 | For example: 59 | 60 | ``` 61 | (-> 62 | ... 63 | (wrap-api \"/api\") 64 | (wrap-file-uploads) 65 | (wrap-keyword-params) 66 | (wrap-multipart-params) 67 | ...) 68 | ``` 69 | " 70 | [handler transit-options] 71 | (fn [req] 72 | (if (transaction-with-uploads? req) 73 | (let [txn (-> req 74 | (upload-transaction) 75 | (transit/transit-str->clj transit-options)) 76 | files (upload-files req) 77 | txn (attach-uploads-to-txn txn files)] 78 | (when-not (seq files) 79 | (log/error "Incoming transaction with uploads had no files attached. See https://book.fulcrologic.com/#err-fu-tx-has-no-files")) 80 | (handler (-> req 81 | (dissoc :params :multipart-params) 82 | (assoc :transit-params txn)))) 83 | (handler req)))) 84 | 85 | -------------------------------------------------------------------------------- /src/main/com/fulcrologic/fulcro/networking/file_url.cljc: -------------------------------------------------------------------------------- 1 | (ns com.fulcrologic.fulcro.networking.file-url 2 | "Support for converting binary network responses to a usable File URL in a browser. This 3 | file is CLJC so the functions exist for SSR, but they do nothing in CLJ.") 4 | 5 | (defn gc-file-url 6 | "Tells the browser to GC the space associated with an in-memory file URL" 7 | [url] 8 | #?(:cljs 9 | (js/window.URL.revokeObjectURL url))) 10 | 11 | (defn raw-response->file-url 12 | "Convert an array-buffer network result (i.e. in a mutation result-action env) into a file URL that can be 13 | used live in the browser. 14 | 15 | `result` - The network result (e.g. the :result key from a mutation env). A map with a `:body` that is an ArrayBuffer. 16 | `mime-type` - The MIME type you want to associate with the file URL. 17 | 18 | NOTE: You should use gc-file-url to release resources when finished. 19 | 20 | To get a binary response use an HTTP remote and use `with-response-type` in your 21 | mutation's remote section: 22 | ``` 23 | (remote [env] (m/with-response-type env :array-buffer) 24 | ``` 25 | 26 | You will also need to modify your middleware to ensure you actually respond with 27 | binary data in the server. 28 | " 29 | [{:keys [body]} mime-type] 30 | #?(:cljs 31 | (let [wrapped-blob (js/Blob. (clj->js [body]) #js {:type mime-type}) 32 | fileURL (js/URL.createObjectURL wrapped-blob)] 33 | fileURL))) 34 | 35 | (defn save-file-url-as! 36 | "Given a file URL and a target filename: generates a DOM link and clicks on it, which should initiate a download in 37 | the browser. 38 | 39 | ALPHA: Not tested in all browsers. Known to work in Chrome. 40 | " 41 | [file-url target-filename] 42 | #?(:cljs 43 | (let [link (js/document.createElement "a")] 44 | (set! (.-target link) "_blank") 45 | (set! (.-href link) file-url) 46 | (set! (.-download link) target-filename) 47 | (.click link)))) 48 | -------------------------------------------------------------------------------- /src/main/com/fulcrologic/fulcro/networking/mock_server_remote.cljs: -------------------------------------------------------------------------------- 1 | (ns com.fulcrologic.fulcro.networking.mock-server-remote 2 | "Simple adapter code that allows you to use a generic parser 'as if' it were a client remote in CLJS." 3 | (:require 4 | [com.fulcrologic.fulcro.algorithms.tx-processing :as txn] 5 | [taoensso.timbre :as log] 6 | [edn-query-language.core :as eql] 7 | [cljs.core.async :as async])) 8 | 9 | (defn mock-http-server 10 | "Create a remote that mocks a Fulcro remote server. 11 | 12 | :parser - A function `(fn [eql-query] async-channel)` that returns a core async channel with the result for the 13 | given eql-query." 14 | [{:keys [parser] :as options}] 15 | (merge options 16 | {:transmit! (fn transmit! [{:keys [active-requests]} {:keys [::txn/ast ::txn/result-handler ::txn/update-handler] :as send-node}] 17 | (let [edn (eql/ast->query ast) 18 | ok-handler (fn [result] 19 | (try 20 | (result-handler (select-keys result #{:transaction :status-code :body :status-text})) 21 | (catch :default e 22 | (log/error e "Result handler failed with an exception. See https://book.fulcrologic.com/#err-msr-res-handler-exc")))) 23 | error-handler (fn [error-result] 24 | (try 25 | (result-handler (merge {:status-code 500} (select-keys error-result #{:transaction :status-code :body :status-text}))) 26 | (catch :default e 27 | (log/error e "Error handler failed with an exception. See https://book.fulcrologic.com/#err-msr-err-handler-exc"))))] 28 | (try 29 | (async/go 30 | (let [result (async/str edn {:metadata? false})] 20 | (.setItem store k value) 21 | true) 22 | (catch :default e 23 | (log/error e "Local storage denied." edn "See https://book.fulcrologic.com/#err-edn-store-denied") 24 | false)))) 25 | (-load-all [this] 26 | (async/go 27 | (try 28 | (mapv 29 | (fn [id] {:id id :value (some-> (.getItem js/localStorage (key-of this id)) (transit/transit-str->clj))}) 30 | (async/ (.getItem js/localStorage k) (transit/transit-str->clj))) 51 | (catch :default e 52 | (log/error e "Load failed. See https://book.fulcrologic.com/#err-edn-store-load-failed") 53 | nil)))) 54 | (-exists? [this id] 55 | (async/go 56 | (let [k (key-of this id) 57 | v (.getItem js/localStorage k)] 58 | (not (or (undefined? v) (nil? v)))))) 59 | (-delete! [this id] 60 | (async/go 61 | (try 62 | (let [k (key-of this id)] 63 | (.removeItem js/localStorage k) 64 | true) 65 | (catch :default e 66 | (log/error e "Delete failed. See https://book.fulcrologic.com/#err-edn-store-delete-failed") 67 | false)))) 68 | (-update-edn! [this id xform] 69 | (async/go 70 | (try 71 | (let [old (async/BrowserEDNStore prefix)) 91 | 92 | (comment 93 | (def store (browser-edn-store "test-store")) 94 | (defonce k (random-uuid)) 95 | (async/go 96 | (js/console.log (async/js props) f)))}) 39 | :clj 40 | (let [context (atom default-value)] 41 | {:raw-context context 42 | :ui-provider (fn [v c] 43 | (let [old-context @context] 44 | (reset! context v) 45 | (try 46 | c 47 | (finally 48 | (reset! context old-context))))) 49 | :ui-consumer (fn [f] (f @context))})))) 50 | 51 | (defn current-context-value 52 | "Usable within render and component lifecycle methods. Gets the context value syncrhonously. 53 | 54 | NOTE: May be flaky among different verions of react. Use at your own risk. Recommended that you use `ui-consumer` 55 | instead." 56 | [context] 57 | #?(:clj @(:raw-context context) 58 | :cljs 59 | (let [context (if (and (map? context) (contains? context :raw-context)) 60 | (:raw-context context) 61 | context)] 62 | (or 63 | (gobj/get context "_currentValue") 64 | (gobj/get context "_currentValue2"))))) 65 | 66 | (defn set-class-context! 67 | "Set the context type on the given component C (component based react)." 68 | [C context] 69 | #?(:clj :noop 70 | :cljs 71 | (set! (.-contextType ^js C) context))) 72 | 73 | (defn use-context 74 | "React hook, but works with Fulcro-wrapped context from this ns." 75 | [wrapped-context] 76 | #?(:clj (some-> wrapped-context :raw-context deref) 77 | :cljs (some-> wrapped-context :raw-context (react/useContext)))) 78 | -------------------------------------------------------------------------------- /src/main/com/fulcrologic/fulcro/react/error_boundaries.cljc: -------------------------------------------------------------------------------- 1 | (ns com.fulcrologic.fulcro.react.error-boundaries 2 | "A macro and predefined component that can create a boundary above which errors cannot propagate. Read the docstring 3 | of `error-boundary` carefully. Works with server-side rendering as well, and provides for development-time retries 4 | to recover after a hot code reload without having to reload the entire page." 5 | #?(:cljs (:require-macros com.fulcrologic.fulcro.react.error-boundaries)) 6 | (:require 7 | [com.fulcrologic.fulcro.components :as comp :refer [defsc]] 8 | [taoensso.encore :as enc] 9 | [taoensso.timbre :as log])) 10 | 11 | (def ^:dynamic *render-error* 12 | "A `(fn [react-element exception] what-to-render)`. Called in order to render an alternate for UI segments that have crashed. 13 | Defaults to a simple div containing the error header and message as standard HTML elements." 14 | (fn [this cause] "There was an error.")) 15 | 16 | (defsc BodyContainer 17 | "A very lightweight container to offset errors so that we can use `error-boundary` to actually catch errors 18 | from subsections of a UI." 19 | [this {:keys [parent render]}] 20 | {:use-hooks? true} 21 | (comp/with-parent-context (or parent this) 22 | (render))) 23 | 24 | (def ui-body-container (comp/factory BodyContainer)) 25 | 26 | (defsc ErrorBoundary [this props] 27 | {:shouldComponentUpdate (fn [_np _ns] true) 28 | :getDerivedStateFromError (fn [error] 29 | {:error true 30 | :cause error}) 31 | :componentDidCatch (fn [_this error _info] (log/error (ex-message error)))} 32 | (let [{:keys [error cause]} (comp/get-state this)] 33 | (if error 34 | (*render-error* this cause) 35 | (ui-body-container props)))) 36 | 37 | (def ui-error-boundary (comp/factory ErrorBoundary)) 38 | 39 | #?(:clj 40 | (defn error-boundary* [body] 41 | `(ui-error-boundary {:parent comp/*parent* 42 | :render #(comp/fragment ~@body)}))) 43 | 44 | #?(:clj 45 | (defn error-boundary-clj [body] 46 | `(try 47 | ~@body 48 | (catch Throwable _e# "Error")))) 49 | 50 | #?(:clj 51 | (defmacro error-boundary 52 | "Wraps the given children in a nested pair of stateless elements that prevent unexpected exceptions from 53 | propagating up the UI tree. Any unexpected rendering or lifecycle errors that happen will be 54 | caught and cause an error message to be shown in place of the children. 55 | 56 | The error boundary rendering is very very simple by default. A simple message that says there was an error. This 57 | is partially because you will always probably want to customize it, and also so there are no dependencies on DOM 58 | so that this macro is usable in Native, where there is no DOM. 59 | 60 | You can clear an error with component local state (see example below), but unless the problem has been corrected 61 | it will just fail again. Therefore, clearing the error is probably not useful except during development when a hot 62 | code reload could actually fix the problem. 63 | 64 | You can also completely take over the error boundary rendering by `binding` or by setting to root binding of *render-error* 65 | via `set!` in cljs, and `alter-var-root` in Clojure. 66 | 67 | For example, in cljs you would add something like this in your entry point: 68 | 69 | ``` 70 | (ns ... 71 | (:require 72 | [com.fulcrologic.fulcro.react.error-boundaries :as eb])) 73 | 74 | ... 75 | 76 | (defn start [] 77 | (set! eb/*render-error* 78 | (fn [this cause] 79 | (dom/div 80 | (dom/h2 :.header \" Unexpected Error \") 81 | (dom/p \"An error occurred while rendering the user interface. \") 82 | (dom/p (str cause)) 83 | (when #?(:clj false :cljs goog.DEBUG) 84 | (dom/button {:onClick (fn [] 85 | (comp/set-state! this {:error false 86 | :cause nil}))} \" Dev Mode: Retry rendering \")))))) 87 | ``` 88 | " 89 | [& body] 90 | (if (enc/compiling-cljs?) 91 | (error-boundary* body) 92 | (error-boundary-clj body)))) 93 | -------------------------------------------------------------------------------- /src/main/com/fulcrologic/fulcro/react/version18.cljc: -------------------------------------------------------------------------------- 1 | (ns com.fulcrologic.fulcro.react.version18 2 | #?@(:cljs 3 | [(:require-macros com.fulcrologic.fulcro.react.version18) 4 | (:require 5 | [com.fulcrologic.fulcro.application] 6 | ["react-dom/client" :as dom-client] 7 | ["react" :as react])])) 8 | 9 | (defn react18-options 10 | "Returns the options that need to be passed to the Fulcro app constructor. See also `with-react18`." 11 | [] 12 | #?(:cljs 13 | (let [reactRoot (volatile! nil)] 14 | {:render-root! (fn [ui-root mount-node] 15 | (when-not @reactRoot 16 | (vreset! reactRoot (dom-client/createRoot mount-node))) 17 | (.render ^js @reactRoot ui-root) 18 | @reactRoot) 19 | :hydrate-root! (fn [ui-root mount-node] (dom-client/hydrateRoot mount-node ui-root))}) 20 | :clj {})) 21 | 22 | (defn with-react18 23 | "Alters the rendering to support React 18" 24 | [app] 25 | #?(:cljs (let [reactRoot (volatile! nil)] 26 | (-> app 27 | (assoc-in 28 | [:com.fulcrologic.fulcro.application/algorithms :com.fulcrologic.fulcro.algorithm/render-root!] 29 | (fn [ui-root mount-node] 30 | (when-not @reactRoot 31 | (vreset! reactRoot (dom-client/createRoot mount-node))) 32 | (.render ^js @reactRoot ui-root) 33 | @reactRoot)) 34 | (assoc-in 35 | [:com.fulcrologic.fulcro.application/algorithms :com.fulcrologic.fulcro.algorithm/hydrate-root!] 36 | (fn [ui-root mount-node] (dom-client/hydrateRoot mount-node ui-root))))) 37 | :clj app)) 38 | -------------------------------------------------------------------------------- /src/main/com/fulcrologic/fulcro/rendering/keyframe_render.cljc: -------------------------------------------------------------------------------- 1 | (ns com.fulcrologic.fulcro.rendering.keyframe-render 2 | "The keyframe optimized render." 3 | (:require 4 | #?(:cljs 5 | ["react-dom" :as react.dom]) 6 | [com.fulcrologic.fulcro.algorithms.denormalize :as fdn] 7 | [com.fulcrologic.fulcro.raw.application :as rapp] 8 | [com.fulcrologic.fulcro.algorithms.lookup :as ah] 9 | [com.fulcrologic.fulcro.components :as comp] 10 | [taoensso.timbre :as log])) 11 | 12 | (defn render-state! 13 | "This function renders given state map over top of the current app. This allows you to render previews of state **without 14 | actually changing the app state**. Used by Inspect for DOM preview. Forces a root-based render with no props diff optimization. 15 | The app must already be mounted. Returns the result of render." 16 | [app state-map] 17 | (comp/enable-forced-refresh! 1000) 18 | (binding [comp/*app* app 19 | comp/*shared* (comp/shared app)] 20 | (let [{:com.fulcrologic.fulcro.application/keys [runtime-atom]} app 21 | {:com.fulcrologic.fulcro.application/keys [root-factory root-class mount-node]} @runtime-atom 22 | r! (or (ah/app-algorithm app :render-root!) #?(:cljs react.dom/render)) 23 | query (comp/get-query root-class state-map) 24 | data-tree (if query 25 | (fdn/db->tree query state-map state-map) 26 | state-map)] 27 | (when (and r! root-factory) 28 | (r! (root-factory data-tree) mount-node))))) 29 | 30 | (defn render! 31 | "Render the UI. The keyframe render runs a full UI query and then asks React to render the root component. 32 | The optimizations for this kind of render are purely those provided by `defsc`'s default 33 | shouldComponentUpdate, which causes component to act like React PureComponent (though the props compare in cljs 34 | is often faster). 35 | 36 | If `:hydrate?` is true it will use the React hydrate functionality (on browsers) to render over 37 | server-rendered content in the DOM. 38 | 39 | If `:force-root? true` is included in the options map then not only will this do a keyframe update, it will also 40 | force all components to return `true` from `shouldComponentUpdate`." 41 | [app {:keys [force-root? hydrate?]}] 42 | (let [{:com.fulcrologic.fulcro.application/keys [runtime-atom state-atom]} app 43 | {:com.fulcrologic.fulcro.application/keys [root-factory root-class mount-node]} @runtime-atom 44 | r! (if hydrate? 45 | (or (ah/app-algorithm app :hydrate-root!) #?(:cljs react.dom/hydrate) #?(:cljs react.dom/render)) 46 | (or (ah/app-algorithm app :render-root!) #?(:cljs react.dom/render))) 47 | state-map @state-atom 48 | query (comp/get-query root-class state-map) 49 | data-tree (if query 50 | (fdn/db->tree query state-map state-map) 51 | state-map) 52 | app-root #?(:clj {} 53 | :cljs (when root-factory 54 | (when force-root? (comp/enable-forced-refresh! 1000)) 55 | (binding [comp/*app* app 56 | comp/*parent* nil 57 | comp/*shared* (comp/shared app)] 58 | (r! (root-factory data-tree) mount-node))))] 59 | (swap! runtime-atom assoc :com.fulcrologic.fulcro.application/app-root app-root) 60 | #?(:cljs app-root))) 61 | -------------------------------------------------------------------------------- /src/main/com/fulcrologic/fulcro/rendering/keyframe_render2.cljc: -------------------------------------------------------------------------------- 1 | (ns com.fulcrologic.fulcro.rendering.keyframe-render2 2 | "Just like keyframe render, but supports `:only-refresh` option." 3 | (:require 4 | [com.fulcrologic.fulcro.rendering.keyframe-render :as kr] 5 | [com.fulcrologic.fulcro.rendering.ident-optimized-render :as ior] 6 | [com.fulcrologic.fulcro.algorithms.denormalize :as fdn] 7 | [com.fulcrologic.fulcro.components :as comp] 8 | [edn-query-language.core :as eql] 9 | [taoensso.timbre :as log])) 10 | 11 | (defn render-stale-components! 12 | "This function tracks the state of the app at the time of prior render in the app's runtime-atom. It 13 | uses that to do a comparison of old vs. current application state (bounded by the needs of on-screen components). 14 | When it finds data that has changed it renders all of the components that depend on that data." 15 | [app options] 16 | (let [{:com.fulcrologic.fulcro.application/keys [runtime-atom]} app 17 | {:com.fulcrologic.fulcro.application/keys [only-refresh]} @runtime-atom 18 | limited-refresh? (seq only-refresh)] 19 | (if limited-refresh? 20 | (let [{limited-idents true} (group-by eql/ident? only-refresh)] 21 | (doseq [i limited-idents] 22 | (ior/render-components-with-ident! app i))) 23 | (kr/render! app options)))) 24 | 25 | (defn render! 26 | "The top-level call for using this optimized render in your application. 27 | 28 | If `:force-root? true` is passed in options, then it forces a `keyframe` root render with 29 | that same option. 30 | 31 | This renderer always does a keyframe render *unless* an `:only-refresh` option is passed to the stack 32 | (usually as an option on `(transact! this [(f)] {:only-refresh [...idents...]})`. In that case the renderer 33 | will ignore *all* data diffing and will target refresh only to the on-screen components that have the listed 34 | ident(s). This allows you to get component-local state refresh rates on transactions that are responding to 35 | events that should really only affect a known set of components (like the input field). 36 | 37 | This option does *not* currently support using query keywords in the refresh set. Only idents." 38 | ([app] 39 | (render! app {})) 40 | ([app {:keys [force-root? root-props-changed?] :as options}] 41 | (if (or force-root? root-props-changed?) 42 | (kr/render! app options) 43 | (try 44 | (render-stale-components! app options) 45 | (catch #?(:clj Exception :cljs :default) e 46 | (log/info "Optimized render failed. Falling back to root render.") 47 | (kr/render! app options)))))) 48 | 49 | -------------------------------------------------------------------------------- /src/main/data_readers.clj: -------------------------------------------------------------------------------- 1 | {js clojure.core/identity} 2 | -------------------------------------------------------------------------------- /src/test/com/fulcrologic/fulcro/algorithms/transit_spec.cljc: -------------------------------------------------------------------------------- 1 | (ns com.fulcrologic.fulcro.algorithms.transit-spec 2 | (:require 3 | [com.fulcrologic.fulcro.algorithms.transit :as ft] 4 | [com.fulcrologic.fulcro.algorithms.tempid :as tempid] 5 | #?@(:cljs 6 | [[cljs.reader :as reader] 7 | [com.cognitect.transit.types :as ty]]) 8 | [cognitect.transit :as t] 9 | [clojure.test :refer [are deftest is]] 10 | [fulcro-spec.core :refer [specification assertions]] 11 | [taoensso.timbre :as log])) 12 | 13 | (declare =>) 14 | 15 | (deftype Cruft [v]) 16 | (deftype Nested [cruft]) 17 | 18 | (ft/install-type-handler! (ft/type-handler Cruft "test/cruft" (fn [^Cruft c] (.-v c)) #(Cruft. %))) 19 | (ft/install-type-handler! (ft/type-handler Nested "test/nested" (fn [^Nested n] (.-cruft n)) #(Nested. %))) 20 | 21 | (specification "transit-clj->str and str->clj" 22 | (let [meta-rtrip (ft/transit-str->clj (ft/transit-clj->str (with-meta {:k (with-meta [] {:y 2})} {:x 1})))] 23 | (assertions 24 | "Encode clojure data structures to strings" 25 | (string? (ft/transit-clj->str {})) => true 26 | (string? (ft/transit-clj->str [])) => true 27 | (string? (ft/transit-clj->str 1)) => true 28 | (string? (ft/transit-clj->str 22M)) => true 29 | (string? (ft/transit-clj->str #{1 2 3})) => true) 30 | (assertions 31 | "Can decode encodings" 32 | (ft/transit-str->clj (ft/transit-clj->str {:a 1})) => {:a 1} 33 | (ft/transit-str->clj (ft/transit-clj->str #{:a 1})) => #{:a 1} 34 | (ft/transit-str->clj (ft/transit-clj->str "Hi")) => "Hi") 35 | (assertions "Preserves metadata" 36 | (meta meta-rtrip) => {:x 1} 37 | (-> meta-rtrip :k meta) => {:y 2}) 38 | (assertions 39 | "Registry auto-includes tempid support" 40 | (tempid/tempid? (ft/transit-str->clj (ft/transit-clj->str (tempid/tempid)))) => true 41 | "Automatically uses the global type registry" 42 | (.-v ^Cruft (ft/transit-str->clj (ft/transit-clj->str (Cruft. 42)))) => 42))) 43 | 44 | (comment 45 | 46 | ;; Notes: Any dumb reader can read ALL POSSIBLE transit-encoded things (everything eventually ends up in a grounded type), 47 | ;; but it just puts them into a generic TaggedValue, which any dumb old writer can turn back into a proper stream. Therefore, 48 | ;; middlemen (like Inspect) can leverage that to interop without having to understand the actual data. 49 | #?(:cljs 50 | (defn tolerant-reader 51 | "Create a transit reader that tolerates any incoming thing. 52 | 53 | - `opts`: (optional) options to pass to `cognitect.transit/reader` (such as data type handlers)." 54 | [] 55 | (t/reader :json {}))) 56 | 57 | (def test-writer #?(:cljs (ft/writer))) 58 | (def dumb-writer #?(:cljs (t/writer :json {}))) 59 | 60 | (let [encoded (t/write test-writer [1 (Nested. (Cruft. 43))]) 61 | decoded (t/read (tolerant-reader) encoded)] 62 | [encoded 63 | (t/write dumb-writer decoded)]) 64 | 65 | (reader/register-tag-parser! 'test/cruft (fn [v] (ty/taggedValue "test/cruft" v))) 66 | (t/write dumb-writer (reader/read-string "#test/cruft 42"))) 67 | 68 | -------------------------------------------------------------------------------- /src/test/com/fulcrologic/fulcro/application_spec.cljc: -------------------------------------------------------------------------------- 1 | (ns com.fulcrologic.fulcro.application-spec 2 | (:require 3 | [fulcro-spec.core :refer [specification provided! when-mocking! assertions behavior when-mocking component =>]] 4 | [clojure.spec.alpha :as s] 5 | [edn-query-language.core :as eql] 6 | [com.fulcrologic.fulcro.specs] 7 | [com.fulcrologic.fulcro.components :as comp] 8 | [com.fulcrologic.fulcro.routing.dynamic-routing :as dr] 9 | [com.fulcrologic.fulcro.ui-state-machines :as uism] 10 | [com.fulcrologic.fulcro.application :as app :refer [fulcro-app]] 11 | [clojure.test :refer [is are deftest]])) 12 | 13 | (deftest application-constructor 14 | (let [app (app/fulcro-app)] 15 | (assertions 16 | "Conforms to the application spec" 17 | (s/explain-data ::app/app app) => nil))) 18 | 19 | (specification "Static extensible configuration" 20 | (let [app (app/fulcro-app {:external-config {::x 1}})] 21 | (assertions 22 | "Allows arbitrary k-v pairs to be added to the static config" 23 | (comp/external-config app ::x) => 1))) 24 | 25 | (specification "Default query elision" 26 | (behavior "Removes ui-namespaced keys that are in props, joins, idents, and mutation joins" 27 | (are [query result] 28 | (= result (eql/ast->query (app/default-global-eql-transform (eql/query->ast query)))) 29 | [:ui/name :x] [:x] 30 | '[(:ui/name {:b 2}) :x] [:x] 31 | [{:ui/name [:b]} :x] [:x] 32 | [[:ui/name 42] :x] [:x] 33 | [{[:ui/name 42] [:t]} :x] [:x] 34 | [:ui/name :x :ui/adsf-b] [:x])) 35 | (behavior "Removes ui and fulcro keys from mutation join queries, but NOT mutation params" 36 | (are [query result] 37 | (= result (eql/ast->query (app/default-global-eql-transform (eql/query->ast query)))) 38 | [{'(f {:ui/param 1 :com.fulcrologic.fulcro/param 42}) [:ui/a :b {:com.fulcrologic.fulcro.core/boo [:y]}]}] 39 | [{'(f {:ui/param 1 :com.fulcrologic.fulcro/param 42}) [:b]}])) 40 | (behavior "Removes items that are namespaced to Fulcro itself" 41 | (are [query result] 42 | (= result (eql/ast->query (app/default-global-eql-transform (eql/query->ast query)))) 43 | [{[::uism/asm-id 42] [:y]} :x] [:x] 44 | [::uism/asm-id :x] [:x] 45 | [{::uism/asm-id [:y]} :x] [:x] 46 | [::dr/id ::dr/current-route [::uism/asm-id '_] :x] [:x]))) 47 | -------------------------------------------------------------------------------- /src/test/com/fulcrologic/fulcro/components_spec.cljc: -------------------------------------------------------------------------------- 1 | (ns com.fulcrologic.fulcro.components-spec 2 | (:require 3 | [com.fulcrologic.fulcro.components :as comp :refer [defsc]] 4 | [com.fulcrologic.fulcro.algorithms.denormalize :as fdn] 5 | #?(:clj [com.fulcrologic.fulcro.dom-server :as dom] 6 | :cljs [com.fulcrologic.fulcro.dom :as dom]) 7 | [fulcro-spec.core :refer [specification assertions when-mocking =>]])) 8 | 9 | (defsc A [this props] 10 | {:ident :person/id 11 | :query [:person/id :person/name] 12 | :initial-state {:person/id 1 :person/name "Tony"} 13 | :extra-data 42} 14 | (dom/div "TODO")) 15 | 16 | (defsc AWithHooks [this props] 17 | {:ident :person/id 18 | :query [:person/id :person/name] 19 | :initial-state {:person/id 1 :person/name "Tony"} 20 | :extra-data 42 21 | :use-hooks? true} 22 | (dom/div "TODO")) 23 | 24 | (defsc B [this props] 25 | {:ident :person/id 26 | :query [:person/id :person/name] 27 | :initial-state (fn [{:keys [id]}] {:person/id id :person/name "Tony"}) 28 | :extra-data 42} 29 | (dom/div "TODO")) 30 | 31 | (def ui-a (comp/factory A)) 32 | 33 | (specification "Component basics" 34 | (let [ui-a (comp/factory A)] 35 | (assertions 36 | "Supports arbitrary option data" 37 | (-> A comp/component-options :extra-data) => 42 38 | "Component class can be detected" 39 | (comp/component-class? A) => true 40 | (comp/component-class? (ui-a {})) => false 41 | "The component name is available from the class" 42 | (comp/component-name A) => (str `A) 43 | "The registry key is available from the class" 44 | (comp/class->registry-key A) => ::A 45 | "Can be used to obtain the ident" 46 | (comp/get-ident A {:person/id 4}) => [:person/id 4] 47 | "Can be used to obtain the query" 48 | (comp/get-query A) => [:person/id :person/name] 49 | (comp/get-query AWithHooks) => [:person/id :person/name] 50 | "Initial state" 51 | (comp/get-initial-state A) => {:person/name "Tony" :person/id 1} 52 | (comp/get-initial-state B {:id 22}) => {:person/name "Tony" :person/id 22} 53 | "The class is available in the registry using a symbol or keyword" 54 | (comp/registry-key->class ::A) => A 55 | (comp/registry-key->class `A) => A))) 56 | 57 | (specification "computed props" 58 | (assertions 59 | "Can be added and extracted on map-based props" 60 | (comp/get-computed (comp/computed {} {:x 1})) => {:x 1} 61 | "Can be added and extracted on vector props" 62 | (comp/get-computed (comp/computed [] {:x 1})) => {:x 1})) 63 | 64 | (specification "newer-props" 65 | (assertions 66 | "Returns the first props if neither are timestamped" 67 | (comp/newer-props {:a 1} {:a 2}) => {:a 1} 68 | "Returns the second props if the first are nil" 69 | (comp/newer-props nil {:a 2}) => {:a 2} 70 | "Returns the newer props if both have times" 71 | (comp/newer-props (fdn/with-time {:a 1} 1) (fdn/with-time {:a 2} 2)) => {:a 2} 72 | (comp/newer-props (fdn/with-time {:a 1} 2) (fdn/with-time {:a 2} 1)) => {:a 1} 73 | "Returns the second props if the times are the same" 74 | (comp/newer-props (fdn/with-time {:a 1} 1) (fdn/with-time {:a 2} 1)) => {:a 2})) 75 | 76 | (specification "classname->class" 77 | (assertions 78 | "Returns from registry under fq keyword" 79 | (nil? (comp/registry-key->class ::A)) => false 80 | "Returns from registry under fq symbol" 81 | (nil? (comp/registry-key->class `A)) => false)) 82 | 83 | (specification "react-type" 84 | #?(:cljs 85 | (assertions 86 | "Returns the class when passed an instance" 87 | (comp/react-type (A.)) => A) 88 | :clj 89 | (assertions 90 | "Returns the class when passed an instance" 91 | (comp/react-type (ui-a {})) => A))) 92 | 93 | (specification "wrap-update-extra-props" 94 | (let [wrapper (comp/wrap-update-extra-props (fn [_ p] (assoc p :X 1))) 95 | updated-props (wrapper A #js {})] 96 | (assertions 97 | "Places extra props in raw props at :fulcro$extra_props" 98 | (comp/isoget updated-props :fulcro$extra_props) => {:X 1}))) 99 | -------------------------------------------------------------------------------- /src/test/com/fulcrologic/fulcro/data_fetch_spec.cljc: -------------------------------------------------------------------------------- 1 | (ns com.fulcrologic.fulcro.data-fetch-spec 2 | (:require 3 | [com.fulcrologic.fulcro.application :as app] 4 | [com.fulcrologic.fulcro.data-fetch :as df] 5 | [com.fulcrologic.fulcro.components :as comp :refer [defsc]] 6 | [clojure.test :refer [is are]] 7 | [fulcro-spec.core :refer [specification behavior assertions provided component when-mocking]] 8 | [com.fulcrologic.fulcro.mutations :as m :refer [defmutation]])) 9 | 10 | 11 | (defsc Person [this props] 12 | {:query [:db/id :username :name] 13 | :ident [:person/id :db/id]}) 14 | 15 | (defsc Comment [this props] 16 | {:query [:db/id :title {:author (comp/get-query Person)}] 17 | :ident [:comments/id :db/id]}) 18 | 19 | (defsc Item [this props] 20 | {:query [:db/id :name {:comments (comp/get-query Comment)}] 21 | :ident [:items/id :db/id]}) 22 | 23 | (defsc InitTestChild [this props] 24 | {:query [:y] 25 | :ident [:child/by-id :y] 26 | :initial-state {:y 2}}) 27 | 28 | (defsc InitTestComponent [this props] 29 | {:initial-state {:x 1 :z :param/z :child {}} 30 | :ident [:parent/by-id :x] 31 | :query [:x :z {:child (comp/get-query InitTestChild)}]}) 32 | 33 | (defsc UnionComponent [this props] 34 | {:initial-state (fn [params] {:a/id 1 :a/name "Alice"}) 35 | :ident (fn [] (cond 36 | (:a/id props) [:a/id (:a/id props)] 37 | (:b/id props) [:b/id (:b/id props)] 38 | :else nil)) 39 | :query (fn [] {:a/id [:a/id :a/name] 40 | :b/id [:b/id :b/another-prop]})}) 41 | 42 | (def app (app/fulcro-app)) 43 | 44 | (specification "Load parameters" 45 | (let [ 46 | query-with-params (:query (df/load-params* app :prop Person {:params {:n 1}})) 47 | ident-query-with-params (:query (df/load-params* app [:person/by-id 1] Person {:params {:n 1}})) 48 | union-query (:query (df/load-params* app :prop UnionComponent {:without #{}}))] 49 | (assertions 50 | "Accepts nil for subquery and params" 51 | (:query (df/load-params* app [:person/by-id 1] nil {})) => [[:person/by-id 1]] 52 | "Constructs query with parameters when subquery is nil" 53 | (:query (df/load-params* app [:person/by-id 1] nil {:params {:x 1}})) => '[([:person/by-id 1] {:x 1})] 54 | "Constructs a JOIN query (without params)" 55 | (:query (df/load-params* app :prop Person {})) => [{:prop (comp/get-query Person)}] 56 | (:query (df/load-params* app [:person/by-id 1] Person {})) => [{[:person/by-id 1] (comp/get-query Person)}] 57 | "Honors target for property-based join" 58 | (:target (df/load-params* app :prop Person {:target [:a :b]})) => [:a :b] 59 | "Constructs a JOIN query (with params on join and prop)" 60 | query-with-params => `[({:prop ~(comp/get-query Person)} {:n 1})] 61 | ident-query-with-params => `[({[:person/by-id 1] ~(comp/get-query Person)} {:n 1})] 62 | "Properly handles components with a union query" 63 | union-query => [{:prop (comp/get-query UnionComponent)}])) 64 | (behavior "can focus the query" 65 | (assertions 66 | (:query (df/load-params* app [:item/by-id 1] Item {:focus [:name {:comments [:title]}]})) 67 | => [{[:item/by-id 1] [:name {:comments [:title]}]}])) 68 | (behavior "can update the query with custom processing" 69 | (assertions 70 | (:query (df/load-params* app [:item/by-id 1] Item {:focus [:name] 71 | :update-query #(conj % :extra)})) 72 | => [{[:item/by-id 1] [:name :extra]}]))) 73 | -------------------------------------------------------------------------------- /src/test/com/fulcrologic/fulcro/macros/defmutation_spec.clj: -------------------------------------------------------------------------------- 1 | (ns com.fulcrologic.fulcro.macros.defmutation-spec 2 | (:require 3 | [com.fulcrologic.fulcro.mutations :as m] 4 | [fulcro-spec.core :refer [specification assertions component]] 5 | [clojure.test :refer :all] 6 | [com.fulcrologic.fulcro.algorithms.lookup :as ah] 7 | [com.fulcrologic.fulcro.components :as comp] 8 | [com.fulcrologic.fulcro.raw.components :as rc])) 9 | 10 | (declare =>) 11 | 12 | (specification "defmutation Macro" 13 | (component "Defining a mutation into a different namespace" 14 | (let [actual (m/defmutation* {} 15 | '(other/boo [params] 16 | (action [env] (swap! state))))] 17 | (assertions 18 | "Emits a defmethod with the proper symbol, action, and default result action." 19 | actual => `(defmethod com.fulcrologic.fulcro.mutations/mutate 'other/boo [~'fulcro-mutation-env-symbol] 20 | (let [~'params (-> ~'fulcro-mutation-env-symbol :ast :params)] 21 | {:result-action (fn [~'env] 22 | (binding [rc/*after-render* true] 23 | (when-let [~'default-action (ah/app-algorithm (:app ~'env) :default-result-action!)] 24 | (~'default-action ~'env)))) 25 | :action (fn ~'action [~'env] 26 | (clojure.core/binding [com.fulcrologic.fulcro.raw.components/*after-render* true] 27 | (~'swap! ~'state)) nil)}))))) 28 | (component "Overridden result action" 29 | (let [actual (m/defmutation* {} 30 | '(other/boo [params] 31 | (result-action [env] (print "Hi"))))] 32 | (assertions 33 | "Uses the user-supplied version of default action" 34 | actual => `(defmethod com.fulcrologic.fulcro.mutations/mutate 'other/boo [~'fulcro-mutation-env-symbol] 35 | (let [~'params (-> ~'fulcro-mutation-env-symbol :ast :params)] 36 | {:result-action (fn ~'result-action [~'env] 37 | (clojure.core/binding [com.fulcrologic.fulcro.raw.components/*after-render* true] 38 | (~'print "Hi")) nil)}))))) 39 | (component "Mutation remotes" 40 | (let [actual (m/defmutation* {} 41 | '(boo [params] 42 | (action [env] (swap! state)) 43 | (remote [env] true) 44 | (rest [env] true))) 45 | method (nth actual 2) 46 | body (nth method 4)] 47 | (assertions 48 | "Converts all sections to lambdas of a defmethod" 49 | (first method) => `defmethod 50 | body => `(let [~'params (-> ~'fulcro-mutation-env-symbol :ast :params)] 51 | {:result-action (fn [~'env] 52 | (binding [rc/*after-render* true] 53 | (when-let [~'default-action (ah/app-algorithm (:app ~'env) :default-result-action!)] 54 | (~'default-action ~'env)))) 55 | :remote (fn ~'remote [~'env] true) 56 | :rest (fn ~'rest [~'env] true) 57 | :action (fn ~'action [~'env] 58 | (binding [rc/*after-render* true] 59 | (~'swap! ~'state)) nil)}))))) 60 | -------------------------------------------------------------------------------- /src/test/com/fulcrologic/fulcro/mutations_spec.cljc: -------------------------------------------------------------------------------- 1 | (ns com.fulcrologic.fulcro.mutations-spec 2 | (:require 3 | [com.fulcrologic.fulcro.raw.components :as rc] 4 | [edn-query-language.core :as eql] 5 | [com.fulcrologic.fulcro.mutations :as m] 6 | [fulcro-spec.core :refer [specification assertions behavior component =>]])) 7 | 8 | (specification "returning" 9 | (behavior "adds a component query to the AST of the given mutation node" 10 | (let [env {:state (atom {}) 11 | :ast (-> (eql/query->ast '[(f)]) 12 | :children 13 | first)} 14 | {new-ast :ast} (m/returning env (rc/nc [:person/id :person/name]))] 15 | (assertions 16 | "sets the query of the parent node" 17 | (:query new-ast) => [:person/id :person/name] 18 | "adds the query children nodes" 19 | (:children new-ast) => [{:type :prop, :dispatch-key :person/id, :key :person/id} 20 | {:type :prop, :dispatch-key :person/name, :key :person/name}]))) 21 | (behavior "supports adding query params to the query" 22 | (let [env {:state (atom {}) 23 | :ast (-> (eql/query->ast '[(f)]) 24 | :children 25 | first)} 26 | {new-ast :ast} (m/returning env (rc/nc [:person/id :person/name]) {:query-params {:page 2}})] 27 | (assertions 28 | "sets the query of the parent node" 29 | (:query new-ast) => '[(:person/id {:page 2}) :person/name] 30 | "adds the query children nodes" 31 | (:children new-ast) => [{:type :prop, :dispatch-key :person/id, :key :person/id :params {:page 2}} 32 | {:type :prop, :dispatch-key :person/name, :key :person/name}])))) 33 | -------------------------------------------------------------------------------- /src/test/com/fulcrologic/fulcro/offline/write_through_mutations_test.cljc: -------------------------------------------------------------------------------- 1 | (ns com.fulcrologic.fulcro.offline.write-through-mutations-test 2 | (:require 3 | [com.fulcrologic.fulcro.offline.tempid-strategy :as t-strat] 4 | [fulcro-spec.core :refer [specification provided! behavior assertions]] 5 | #?(:clj [com.fulcrologic.fulcro.algorithms.tempid :as tempid] 6 | :cljs [com.fulcrologic.fulcro.algorithms.tempid :as tempid :refer [TempId]])) 7 | #?(:clj 8 | (:import 9 | (com.fulcrologic.fulcro.algorithms.tempid TempId)))) 10 | 11 | (declare =>) 12 | 13 | (specification "TempIDisRealIDStrategy" 14 | (let [id1 (tempid/tempid) 15 | id2 (tempid/tempid) 16 | id3 (tempid/tempid) 17 | id4 (tempid/tempid) 18 | ids [id1 id2 id3 id4] 19 | real-id (fn [^TempId id] (.-id id)) 20 | txn `[(~'f ~{:id id1 21 | :things [{:id id2 :child {:id id3}} {:id id4}]})] 22 | strategy (t-strat/->TempIDisRealIDStrategy) 23 | expected-resolution (zipmap ids (map real-id ids)) 24 | resolved-ids (t-strat/-resolve-tempids strategy txn)] 25 | (assertions 26 | "Generates a tempid remapping that uses the IDs of the tempids themselves as real IDs" 27 | resolved-ids => expected-resolution))) 28 | -------------------------------------------------------------------------------- /src/test/com/fulcrologic/fulcro/raw/components_spec.cljc: -------------------------------------------------------------------------------- 1 | (ns com.fulcrologic.fulcro.raw.components-spec 2 | (:require 3 | [com.fulcrologic.fulcro.raw.components :as rc] 4 | [com.fulcrologic.fulcro.components :refer [defsc]] 5 | [fulcro-spec.core :refer [specification assertions behavior component =>]] 6 | [edn-query-language.core :as eql])) 7 | 8 | (defsc Product [_ _] 9 | {:query [:product/id :product/name] 10 | :ident :product/id}) 11 | 12 | (specification "nc" 13 | (component "Basic usage" 14 | (let [item-c (rc/nc [:item/id {:item/product [:product/id :product/name]}]) 15 | product-c (rc/get-subquery-component item-c [:item/product])] 16 | (assertions 17 | "Generates a component class" 18 | (rc/component-class? item-c) => true 19 | "Generates ident function for top" 20 | (rc/get-ident item-c {:item/id 1}) => [:item/id 1] 21 | "Generates ident for subcomponent" 22 | (rc/get-ident product-c {:product/id 2}) => [:product/id 2] 23 | "Generates correct queries" 24 | (rc/get-query item-c) => [:item/id {:item/product [:product/id :product/name]}] 25 | (rc/get-query product-c) => [:product/id :product/name]))) 26 | (component "Recursive usage" 27 | (let [product-c (rc/nc [:product/id :product/name] 28 | {:componentName ::Sample}) 29 | item-c (rc/nc [:item/id {:item/product (rc/get-query product-c)}]) 30 | nested-product-c (rc/get-subquery-component item-c [:item/product])] 31 | (assertions 32 | "Includes named components in the registry" 33 | (rc/registry-key->class ::Sample) => product-c 34 | "Nested component is the one we find through the query" 35 | nested-product-c => product-c))) 36 | (component "Composition with normal components" 37 | (let [item-c (rc/nc [:item/id {:item/product (rc/get-query Product)}]) 38 | nested-product-c (rc/get-subquery-component item-c [:item/product])] 39 | (assertions 40 | "Uses the component in the nested location" 41 | (rc/registry-key->class ::Product) => nested-product-c 42 | "Resulting query is correct" 43 | (rc/get-query item-c) => [:item/id {:item/product [:product/id :product/name]}]))) 44 | (component "Union queries" 45 | (let [union-item (rc/nc {:item/id [:item/id :item/content] 46 | :picture/id [:picture/id :picture/url]} 47 | {:ident (fn [this props] 48 | (if-let [id (:item/id props)] 49 | [:item/id id] 50 | [:picture/id (:picture/id props)]))}) 51 | todo-component (rc/nc [:todo/id :todo/text] {:componentName ::todo}) 52 | comment-component (rc/nc [:comment/id :comment/text] {:componentName ::comment}) 53 | list-item-component (rc/nc {:comment/id (rc/get-query comment-component) :todo/id (rc/get-query todo-component)})] 54 | (assertions 55 | "Returns a union query" 56 | (rc/get-query union-item) => {:item/id [:item/id :item/content] 57 | :picture/id [:picture/id :picture/url]} 58 | "The nested target components are generated as anonymous components" 59 | (some-> (rc/get-query union-item) :item/id (meta) :component (rc/component-class?)) => true 60 | (some-> (rc/get-query union-item) :picture/id (meta) :component (rc/component-class?)) => true 61 | "The nested components have the expected queries" 62 | (some-> (rc/get-query union-item) :item/id (meta) :component rc/get-query) => [:item/id :item/content] 63 | (some-> (rc/get-query union-item) :picture/id (meta) :component rc/get-query) => [:picture/id :picture/url] 64 | "The nested components with names are still named" 65 | (-> (rc/get-query list-item-component) :comment/id (meta) :component rc/class->registry-key) => ::comment 66 | (-> (rc/get-query list-item-component) :todo/id (meta) :component rc/class->registry-key) => ::todo)))) 67 | -------------------------------------------------------------------------------- /src/test/com/fulcrologic/fulcro/routing/legacy_ui_routers_spec.cljc: -------------------------------------------------------------------------------- 1 | (ns com.fulcrologic.fulcro.routing.legacy-ui-routers-spec 2 | (:require 3 | [fulcro-spec.core :refer [specification assertions component]] 4 | [com.fulcrologic.fulcro.routing.legacy-ui-routers :as fr] 5 | [com.fulcrologic.fulcro.algorithms.normalize :refer [tree->db]] 6 | [com.fulcrologic.fulcro.components :as comp])) 7 | 8 | (comp/defsc SimpleTarget [_ {:PAGE/keys [ident id]}] 9 | {:query [:PAGE/id 10 | :PAGE/ident] 11 | :ident (fn [] [ident id]) 12 | :initial-state (fn [_] 13 | {:PAGE/id :PAGE/simple-target 14 | :PAGE/ident :PAGE/simple-target})}) 15 | 16 | (fr/defsc-router SimpleRouter [_ {:PAGE/keys [ident id]}] 17 | {:default-route SimpleTarget 18 | :ident (fn [] [ident id]) 19 | :router-targets {:PAGE/simple-target SimpleTarget} 20 | :router-id :PAGE/root-router}) 21 | 22 | (specification "defsc-router Macro" 23 | (component "Basic feature access" 24 | (assertions 25 | "Just returns it's query" 26 | (comp/get-query SimpleRouter) 27 | => [::fr/id 28 | {::fr/current-route {:PAGE/simple-target [:PAGE/id :PAGE/ident]}}] 29 | "Router ident" 30 | (comp/get-ident SimpleRouter {}) 31 | => [:fulcro.client.routing.routers/by-id :PAGE/root-router]))) 32 | -------------------------------------------------------------------------------- /src/test/com/fulcrologic/fulcro/server/dont_require_me.clj: -------------------------------------------------------------------------------- 1 | (ns com.fulcrologic.fulcro.server.dont-require-me) 2 | -------------------------------------------------------------------------------- /src/test/config/defaults.edn: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /src/test/config/other.edn: -------------------------------------------------------------------------------- 1 | {:some-key :some-default-val} -------------------------------------------------------------------------------- /src/test/config/test.edn: -------------------------------------------------------------------------------- 1 | {:datomic {:dbs {:protocol-support {:url "datomic:mem://protocol-support" 2 | :schema "migrations" 3 | :auto-migrate true 4 | :auto-drop true} 5 | :protocol-support-2 {:url "datomic:mem://protocol-support-2" 6 | :schema "migrations" 7 | :auto-migrate true 8 | :auto-drop true} 9 | :protocol-support-3 {:url "datomic:mem://protocol-support-3" 10 | :schema "migrations" 11 | :auto-migrate true 12 | :auto-drop true}}}} 13 | -------------------------------------------------------------------------------- /src/todomvc/fulcro_todomvc/app.cljs: -------------------------------------------------------------------------------- 1 | (ns fulcro-todomvc.app 2 | (:require 3 | [com.fulcrologic.fulcro.algorithms.tx-processing.batched-processing :as btxn] 4 | [com.fulcrologic.fulcro.algorithms.tx-processing.synchronous-tx-processing :as sync] 5 | [com.fulcrologic.fulcro.application :as app] 6 | [com.fulcrologic.fulcro.networking.http-remote :as http] 7 | [com.fulcrologic.fulcro.react.version18 :refer [with-react18]])) 8 | 9 | (defonce remote #_(fws/fulcro-websocket-remote {:auto-retry? true 10 | :request-timeout-ms 10000}) (http/fulcro-http-remote {})) 11 | 12 | #_(defonce app (sync/with-synchronous-transactions 13 | (app/fulcro-app {:remotes {:remote remote}}) 14 | #{:remote})) 15 | 16 | (defonce app (with-react18 17 | (btxn/with-batched-reads 18 | (app/fulcro-app {:remotes {:remote remote 19 | :other remote}}) 20 | #{:remote :other}))) 21 | 22 | #_(defonce app (app/fulcro-app {:remotes {:remote remote}})) 23 | -------------------------------------------------------------------------------- /src/todomvc/fulcro_todomvc/custom_types.cljc: -------------------------------------------------------------------------------- 1 | (ns fulcro-todomvc.custom-types 2 | (:require 3 | [com.fulcrologic.fulcro.algorithms.transit :as transit])) 4 | 5 | (deftype Point [x y]) 6 | 7 | (defn install! [] 8 | (transit/install-type-handler! 9 | (transit/type-handler Point "geo/point" 10 | (fn [^Point p] [(.-x p) (.-y p)]) 11 | (fn [[x y]] (Point. x y))))) 12 | -------------------------------------------------------------------------------- /src/todomvc/fulcro_todomvc/main.cljs: -------------------------------------------------------------------------------- 1 | (ns fulcro-todomvc.main 2 | (:require 3 | [fulcro-todomvc.ui :as ui] 4 | [com.fulcrologic.fulcro.networking.websockets :as fws] 5 | [com.fulcrologic.fulcro.algorithms.timbre-support :refer [console-appender prefix-output-fn]] 6 | [com.fulcrologic.fulcro.application :as app] 7 | [com.fulcrologic.fulcro.data-fetch :as df] 8 | [com.fulcrologic.devtools.common.target :refer [ido]] 9 | [fulcro.inspect.tool :as it] 10 | [taoensso.timbre :as log] 11 | [fulcro-todomvc.custom-types :as custom-types] 12 | [fulcro-todomvc.app :refer [app]])) 13 | 14 | ;; Have to be installed before we create websockets 15 | (defonce prevent-again (custom-types/install!)) 16 | 17 | (defn ^:export start [] 18 | (app/mount! app ui/Root "app") 19 | (ido 20 | (it/add-fulcro-inspect! app)) 21 | (df/load! app [:list/id 1] ui/TodoList) 22 | (log/merge-config! {:output-fn prefix-output-fn 23 | :appenders {:console (console-appender)}})) 24 | 25 | (comment 26 | (app/set-root! app ui/Root {:initialize-state? true}) 27 | (app/mounted? app) 28 | (df/load! app [:list/id 1] ui/TodoList) 29 | (app/mount! app ui/Root "app" {:initialize-state? false}) 30 | @(::app/state-atom app) 31 | (fws/stop! remote) 32 | ) 33 | -------------------------------------------------------------------------------- /src/todomvc/fulcro_todomvc/server.cljs: -------------------------------------------------------------------------------- 1 | (ns fulcro-todomvc.server 2 | (:require 3 | [com.fulcrologic.fulcro.mutations :as m :refer [defmutation]] 4 | [clojure.core.async :as async] 5 | [com.wsscode.pathom.core :as p] 6 | [com.wsscode.pathom.connect :as pc] 7 | [taoensso.timbre :as log])) 8 | 9 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 10 | ;; Pretend server 11 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 12 | 13 | (def item-db (atom {1 {:item/id 1 14 | :item/label "Item 1" 15 | :item/complete false} 16 | 2 {:item/id 2 17 | :item/label "Item 2" 18 | :item/complete false} 19 | 3 {:item/id 3 20 | :item/label "Item 3" 21 | :item/complete false}})) 22 | 23 | (pc/defmutation todo-new-item [env {:keys [id list-id text]}] 24 | {::pc/sym `fulcro-todomvc.api/todo-new-item 25 | ::pc/params [:list-id :id :text] 26 | ::pc/output [:item/id]} 27 | (log/info "New item on server") 28 | (let [new-id (random-uuid)] 29 | (swap! item-db assoc new-id {:item/id new-id :item/label text :item/complete false}) 30 | {:tempids {id new-id} 31 | :item/id new-id})) 32 | 33 | ;; How to go from :person/id to that person's details 34 | (pc/defresolver list-resolver [env params] 35 | {::pc/input #{:list/id} 36 | ::pc/output [:list/title {:list/items [:item/id]}]} 37 | ;; normally you'd pull the person from the db, and satisfy the listed 38 | ;; outputs. For demo, we just always return the same person details. 39 | {:list/title "The List" 40 | :list/items [{:item/id 1} {:item/id 2} {:item/id 3}]}) 41 | 42 | ;; how to go from :address/id to address details. 43 | (pc/defresolver item-resolver [env {:keys [item/id] :as params}] 44 | {::pc/input #{:item/id} 45 | ::pc/output [:item/complete :item/label]} 46 | (get @item-db id)) 47 | 48 | ;; define a list with our resolvers 49 | (def my-resolvers [list-resolver item-resolver todo-new-item]) 50 | 51 | ;; setup for a given connect system 52 | (def parser 53 | (p/parallel-parser 54 | {::p/env {::p/reader [p/map-reader 55 | pc/parallel-reader 56 | pc/open-ident-reader 57 | p/env-placeholder-reader]} 58 | ::p/mutate pc/mutate-async 59 | ::p/plugins [(pc/connect-plugin {::pc/register my-resolvers}) 60 | (p/post-process-parser-plugin p/elide-not-found) 61 | p/error-handler-plugin]})) 62 | 63 | 64 | -------------------------------------------------------------------------------- /src/todomvc/fulcro_todomvc/websocket_server.clj: -------------------------------------------------------------------------------- 1 | (ns fulcro-todomvc.websocket-server 2 | (:require 3 | [com.fulcrologic.fulcro.server.api-middleware :refer [not-found-handler]] 4 | [com.fulcrologic.fulcro.networking.websockets :as fws] 5 | [fulcro-todomvc.custom-types :as custom-types] 6 | [immutant.web :as web] 7 | [fulcro-todomvc.server :refer [parser]] 8 | [ring.middleware.content-type :refer [wrap-content-type]] 9 | [ring.middleware.not-modified :refer [wrap-not-modified]] 10 | [ring.middleware.resource :refer [wrap-resource]] 11 | [ring.middleware.params :refer [wrap-params]] 12 | [ring.middleware.keyword-params :refer [wrap-keyword-params]] 13 | [ring.util.response :refer [response file-response resource-response]] 14 | [taoensso.sente.server-adapters.immutant :refer [get-sch-adapter]])) 15 | 16 | (def server (atom nil)) 17 | 18 | (defn http-server [] 19 | (custom-types/install!) 20 | (let [websockets (fws/start! (fws/make-websockets 21 | (fn [query] (parser {} query)) 22 | {:http-server-adapter (get-sch-adapter) 23 | ;; See Sente for CSRF instructions 24 | :sente-options {:csrf-token-fn nil}})) 25 | middleware (-> not-found-handler 26 | (fws/wrap-api websockets) 27 | wrap-keyword-params 28 | wrap-params 29 | (wrap-resource "public") 30 | wrap-content-type 31 | wrap-not-modified) 32 | result (web/run middleware {:host "0.0.0.0" 33 | :port 8080})] 34 | (reset! server 35 | (fn [] 36 | (fws/stop! websockets) 37 | (web/stop result))))) 38 | 39 | (comment 40 | 41 | ;; start 42 | (http-server) 43 | 44 | ;; stop 45 | (@server)) 46 | -------------------------------------------------------------------------------- /src/todomvc/other_demos/multi_root_sample.cljs: -------------------------------------------------------------------------------- 1 | (ns other-demos.multi-root-sample 2 | (:require 3 | [com.fulcrologic.fulcro.rendering.multiple-roots-renderer :as mroot] 4 | [com.fulcrologic.fulcro.application :as app] 5 | [com.fulcrologic.fulcro.components :as comp :refer [defsc]] 6 | [com.fulcrologic.fulcro.application :as app] 7 | [com.fulcrologic.fulcro.dom :as dom] 8 | [com.fulcrologic.fulcro.mutations :as m] 9 | [taoensso.timbre :as log] 10 | [com.fulcrologic.fulcro.react.hooks :as hooks])) 11 | 12 | (declare AltRootPlainClass app) 13 | 14 | (defsc OtherChild [this {:keys [:other/id :other/n] :as props}] 15 | {:query [:other/id :other/n] 16 | :ident :other/id 17 | :initial-state {:other/id :param/id :other/n :param/n}} 18 | (dom/div 19 | (dom/button 20 | {:onClick #(m/set-integer! this :other/n :value (inc n))} 21 | (str n)))) 22 | 23 | (def ui-other-child (comp/factory OtherChild {:keyfn :other/id})) 24 | 25 | (defsc AltRoot [this props] 26 | {:use-hooks? true} 27 | (let [id (hooks/use-generated-id) ; Generate an ID to use with floating root 28 | ;; mount a floating root that renders OtherChild 29 | factory (hooks/use-fulcro-mount this {:initial-state-params {:id id :n 1} 30 | :child-class OtherChild})] 31 | ;; Install a GC handler that will clean up the generated data of OtherChild when this component unmounts 32 | (hooks/use-gc this [:other/id id] #{}) 33 | (dom/div 34 | (dom/h4 "ALTERNATE ROOT") 35 | (when factory 36 | (factory props))))) 37 | 38 | (def ui-alt-root (mroot/floating-root-factory AltRoot)) 39 | 40 | (defsc Child [this {:child/keys [id name] :as props}] 41 | {:query [:child/id :child/name] 42 | :ident :child/id 43 | :initial-state {:child/id :param/id :child/name :param/name}} 44 | (dom/div 45 | (dom/h2 "Regular Tree") 46 | (dom/label "Child: ") 47 | (dom/input {:value (or name "") 48 | :onChange (fn [evt] 49 | (let [v (.. evt -target -value)] 50 | (comp/transact! this 51 | [(m/set-props {:child/name v})] 52 | {:only-refresh [(comp/get-ident this)]})))}) 53 | (dom/div 54 | (if (= 1 id) 55 | ;; EACH child renders a floating root component 56 | (ui-alt-root) 57 | (dom/create-element AltRootPlainClass))))) 58 | 59 | (def ui-child (comp/factory Child {:keyfn :child/id})) 60 | 61 | (defsc Root [this {:keys [children] :as props}] 62 | {:query [{:children (comp/get-query Child)}] 63 | :initial-state {:children [{:id 1 :name "Joe"} 64 | {:id 2 :name "Sally"}]}} 65 | (let [show? (comp/get-state this :show?)] 66 | (dom/div 67 | (dom/button {:onClick (fn [] (comp/set-state! this {:show? (not show?)}))} "Toggle") 68 | (when show? 69 | ;; Two children 70 | (mapv ui-child children))))) 71 | 72 | (defonce app (app/fulcro-app {:optimized-render! mroot/render!})) 73 | (comment 74 | (-> app ::app/runtime-atom deref ::app/indexes) 75 | ) 76 | (def AltRootPlainClass (mroot/floating-root-react-class AltRoot app)) 77 | 78 | (defn start [] 79 | (app/mount! app Root "app")) 80 | -------------------------------------------------------------------------------- /src/workspaces/com/fulcrologic/fulcro/cards/error_boundary_cards.cljs: -------------------------------------------------------------------------------- 1 | (ns com.fulcrologic.fulcro.cards.error-boundary-cards 2 | (:require 3 | [nubank.workspaces.card-types.fulcro3 :as ct.fulcro] 4 | [nubank.workspaces.core :as ws] 5 | [com.fulcrologic.fulcro.application :as app] 6 | [com.fulcrologic.fulcro.components :as comp :refer [defsc]] 7 | [com.fulcrologic.fulcro.dom :as dom] 8 | [com.fulcrologic.fulcro.mutations :as m] 9 | [taoensso.timbre :as log])) 10 | 11 | 12 | (defsc BadActor [this {:keys [id] :as props}] 13 | {:query [:id] 14 | :ident :id 15 | :componentDidMount (fn [this] 16 | (when (and 17 | (not (comp/get-computed this :reset?)) 18 | (= 2 (:id (comp/props this)))) 19 | (throw (ex-info "mount Craptastic!" {})))) 20 | :componentWillUnmount (fn [this] (log/spy :info (comp/props this)) 21 | (when (= 3 (:id (comp/props this))) 22 | (throw (ex-info "unmount Craptastic!" {})))) 23 | :initial-state {:id :param/id}} 24 | (if (and (= id 4) 25 | (not (comp/get-computed this :reset?))) 26 | (throw (ex-info "Render craptastic!" {})) 27 | (dom/div "Actor"))) 28 | 29 | (def ui-bad-actor (comp/computed-factory BadActor {:keyfn :id})) 30 | 31 | (defsc Child [this {:child/keys [id name actor] :as props}] 32 | {:query [:child/id :child/name {:child/actor (comp/get-query BadActor)}] 33 | :ident :child/id 34 | :componentDidCatch (fn [this err info] (log/spy :error [err info])) 35 | :getDerivedStateFromError (fn [error] 36 | (log/spy :info error) 37 | {:error? true}) 38 | :initial-state {:child/id :param/id :child/name :param/name 39 | :child/actor {:id :param/id}}} 40 | (if (log/spy :info (comp/get-state this :error?)) 41 | (dom/div 42 | "There was an error in this part of the UI." 43 | (dom/button {:onClick #(comp/set-state! this {:error? false})} "Retry")) 44 | (dom/div 45 | (dom/label "Child: " name) 46 | (ui-bad-actor actor {:reset? (contains? (comp/get-state this) :error?)})))) 47 | 48 | (def ui-child (comp/factory Child {:keyfn :child/id})) 49 | 50 | (defsc Root [this {:keys [children]}] 51 | {:query [{:children (comp/get-query Child)}] 52 | :initLocalState (fn [] {:idx 0}) 53 | :initial-state {:children [{:id 1 :name "Joe"} 54 | {:id 2 :name "Sally"} 55 | {:id 3 :name "Bob"} 56 | {:id 4 :name "Alice"}]}} 57 | (let [idx (comp/get-state this :idx)] 58 | (dom/div 59 | (dom/button {:onClick (fn [] (comp/set-state! this {:idx (min 3 (inc idx))}))} "Next actor") 60 | (dom/button {:onClick (fn [] (comp/set-state! this {:idx (max 0 (dec idx))}))} "Prior actor") 61 | (ui-child (get children idx))))) 62 | 63 | (ws/defcard error-boundary-card 64 | (ct.fulcro/fulcro-card 65 | {::ct.fulcro/wrap-root? false 66 | ::ct.fulcro/root Root 67 | ::ct.fulcro/app {}})) 68 | -------------------------------------------------------------------------------- /src/workspaces/com/fulcrologic/fulcro/cards/form_cards.cljs: -------------------------------------------------------------------------------- 1 | (ns com.fulcrologic.fulcro.cards.form-cards 2 | (:require 3 | [com.fulcrologic.fulcro.algorithms.form-state :as fs] 4 | [com.fulcrologic.fulcro.algorithms.tx-processing :as txn] 5 | [com.fulcrologic.fulcro.components :refer [defsc]] 6 | [com.fulcrologic.fulcro.data-fetch :as df] 7 | [com.fulcrologic.fulcro.dom :as dom] 8 | [com.fulcrologic.fulcro.mutations :as m] 9 | [edn-query-language.core :as eql] 10 | [nubank.workspaces.card-types.fulcro3 :as ct.fulcro] 11 | [nubank.workspaces.core :as ws] 12 | [taoensso.timbre :as log])) 13 | 14 | (defonce remote {:transmit! (fn transmit! [_ {:keys [::txn/ast ::txn/result-handler ::txn/update-handler] :as send-node}] 15 | (let [edn (eql/ast->query ast) 16 | ok-handler (fn [result] 17 | (try 18 | (result-handler (select-keys result #{:transaction :status-code :body :status-text})) 19 | (catch :default e 20 | (log/error e "Result handler failed with an exception."))))] 21 | (ok-handler {:transaction edn :status-code 200 :body {[:form/id 42] {:form/id 42 :form/name "Sam"}}})))}) 22 | 23 | (defsc Form [this {:form/keys [name] :as props}] 24 | {:query [:form/id :form/name fs/form-config-join] 25 | :ident :form/id 26 | :initial-state {:form/id 42 27 | :form/name "Sam"} 28 | :form-fields #{:form/name} 29 | :pre-merge (fn [{:keys [data-tree]}] 30 | (fs/add-form-config Form data-tree))} 31 | (dom/div 32 | (dom/button {:onClick #(df/load! this [:form/id 42] Form)} "Load!") 33 | (dom/label "Name") 34 | (dom/input {:value name 35 | :onChange #(m/set-string! this :form/name :event %)}))) 36 | 37 | (ws/defcard form-pre-merge-sample 38 | (ct.fulcro/fulcro-card 39 | {::ct.fulcro/wrap-root? true 40 | ::ct.fulcro/root Form 41 | ::ct.fulcro/app {:remotes {:remote remote}}})) 42 | -------------------------------------------------------------------------------- /src/workspaces/com/fulcrologic/fulcro/cards/multi_root_cards.cljs: -------------------------------------------------------------------------------- 1 | (ns com.fulcrologic.fulcro.cards.multi-root-cards 2 | (:require 3 | [com.fulcrologic.fulcro.components :as comp :refer [defsc]] 4 | [com.fulcrologic.fulcro.dom :as dom] 5 | [com.fulcrologic.fulcro.mutations :as m] 6 | [com.fulcrologic.fulcro.react.hooks :as hooks] 7 | [com.fulcrologic.fulcro.rendering.multiple-roots-renderer :as mroot] 8 | [nubank.workspaces.card-types.fulcro3 :as ct.fulcro] 9 | [nubank.workspaces.core :as ws] 10 | [taoensso.timbre :as log])) 11 | 12 | (defsc OtherChild [this {:keys [:other/id :other/n] :as props}] 13 | {:query [:other/id :other/n] 14 | :ident :other/id 15 | :initial-state {:other/id :param/id :other/n :param/n}} 16 | (log/info "OtherChild" (some-> comp/*parent* (comp/component-name)) (comp/depth this)) 17 | (dom/div 18 | (dom/button 19 | {:onClick #(m/set-integer! this :other/n :value (inc n))} 20 | (str n)))) 21 | 22 | (def ui-other-child (comp/factory OtherChild {:keyfn :other/id})) 23 | 24 | (defsc AltRoot [this props] 25 | {:use-hooks? true} 26 | (log/info "AltRoot" (some-> comp/*parent* (comp/component-name)) (comp/depth this)) 27 | (let [id (hooks/use-generated-id) ; Generate an ID to use with floating root 28 | ;; mount a floating root that renders OtherChild 29 | factory (hooks/use-fulcro-mount this {:initial-state-params {:id id :n 1} 30 | :child-class OtherChild})] 31 | ;; Install a GC handler that will clean up the generated data of OtherChild when this component unmounts 32 | (hooks/use-gc this [:other/id id] #{}) 33 | (dom/div 34 | (dom/h4 "ALTERNATE ROOT") 35 | (when factory 36 | (factory props))))) 37 | 38 | (def ui-alt-root (mroot/floating-root-factory AltRoot)) 39 | 40 | (defsc Child [this {:child/keys [id name] :as props}] 41 | {:query [:child/id :child/name] 42 | :ident :child/id 43 | :initial-state {:child/id :param/id :child/name :param/name}} 44 | (log/info "Child" (some-> comp/*parent* (comp/component-name)) (comp/depth this)) 45 | (dom/div 46 | (dom/h2 "Regular Tree") 47 | (dom/label "Child: ") 48 | (dom/input {:value (or name "") 49 | :onChange (fn [evt] 50 | (let [v (.. evt -target -value)] 51 | (comp/transact! this 52 | [(m/set-props {:child/name v})] 53 | {:only-refresh [(comp/get-ident this)]})))}) 54 | (ui-alt-root))) 55 | 56 | (def ui-child (comp/factory Child {:keyfn :child/id})) 57 | 58 | (defsc Root [this {:keys [children] :as props}] 59 | {:query [{:children (comp/get-query Child)}] 60 | :initial-state {:children [{:id 1 :name "Joe"} 61 | {:id 2 :name "Sally"}]}} 62 | (log/info "Root" (some-> comp/*parent* (comp/component-name)) (comp/depth this)) 63 | (let [show? (comp/get-state this :show?)] 64 | (dom/div 65 | (dom/button {:onClick (fn [] (comp/set-state! this {:show? (not show?)}))} "Toggle") 66 | (when show? 67 | ;; Two children 68 | (mapv ui-child children))))) 69 | 70 | (ws/defcard floating-root-demo-card 71 | (ct.fulcro/fulcro-card 72 | {::ct.fulcro/wrap-root? false 73 | ::ct.fulcro/root Root 74 | ::ct.fulcro/app {}})) 75 | -------------------------------------------------------------------------------- /src/workspaces/com/fulcrologic/fulcro/cards/ref_cards.cljs: -------------------------------------------------------------------------------- 1 | (ns com.fulcrologic.fulcro.cards.ref-cards 2 | (:require 3 | ["react" :as react] 4 | [nubank.workspaces.card-types.fulcro3 :as ct.fulcro] 5 | [nubank.workspaces.core :as ws] 6 | [com.fulcrologic.fulcro.components :as comp :refer [defsc]] 7 | [com.fulcrologic.fulcro.dom :as dom :refer [div input]] 8 | [com.fulcrologic.fulcro.react.hooks :as hooks] 9 | [com.fulcrologic.fulcro.mutations :as m] 10 | [com.fulcrologic.fulcro.dom.events :as evt])) 11 | 12 | (defsc CustomInput [this {:keys [forward-ref label value onChange] :as props}] 13 | {} 14 | (dom/div :.ui.field 15 | (input (cond-> {:name (str label) 16 | :value (str value)} 17 | forward-ref (assoc :ref forward-ref) 18 | onChange (assoc :onChange onChange))) 19 | (dom/label {:htmlFor (str label)} label))) 20 | 21 | (def ui-custom-input (comp/factory CustomInput)) 22 | 23 | (def HooksUI (comp/sc ::HooksUI {:use-hooks? true} 24 | (fn [this props] 25 | (let [[v setv!] (hooks/use-state "") 26 | input-ref (hooks/use-ref nil)] 27 | (hooks/use-effect 28 | (fn [] 29 | (let [input (.-current input-ref)] 30 | (when input 31 | (js/console.log input) 32 | (.focus input))) 33 | 34 | (fn [])) 35 | [(.-current input-ref)]) 36 | (div 37 | (dom/h4 "My Form") 38 | (ui-custom-input 39 | {:label "My Input" 40 | :value v 41 | :forward-ref input-ref 42 | :onChange (fn [v] (setv! v))})))))) 43 | 44 | (ws/defcard ref-hooks-demo-card 45 | (ct.fulcro/fulcro-card 46 | {::ct.fulcro/wrap-root? false 47 | ::ct.fulcro/root HooksUI})) 48 | 49 | (def StdUI (comp/sc 50 | ::StdUI 51 | {:query [:id :value] 52 | :ident :id 53 | :initLocalState (fn [^js this props] (set! (.-inputref this) (react/createRef))) 54 | :initial-state (fn [_] {:id 42 55 | :value "Bob"}) 56 | :componentDidMount (fn [^js this] 57 | (let [input-ref (.-current (.-inputref this))] 58 | (when input-ref 59 | (.focus input-ref))))} 60 | (fn render* [^js this {:keys [value]}] 61 | (let [input-ref (.-inputref this)] 62 | (div 63 | (dom/h4 "My Form") 64 | (ui-custom-input 65 | {:label "My Input" 66 | :value value 67 | :forward-ref input-ref 68 | :onChange (fn [v] (m/set-string!! this :value :value (evt/target-value v)))})))))) 69 | 70 | (ws/defcard ref-demo-card 71 | (ct.fulcro/fulcro-card 72 | {::ct.fulcro/wrap-root? true 73 | ::ct.fulcro/root StdUI})) 74 | -------------------------------------------------------------------------------- /src/workspaces/com/fulcrologic/fulcro/cards/source_annotation_cards.cljs: -------------------------------------------------------------------------------- 1 | (ns com.fulcrologic.fulcro.cards.source-annotation-cards 2 | (:require 3 | [nubank.workspaces.card-types.fulcro3 :as ct.fulcro] 4 | [nubank.workspaces.core :as ws] 5 | [com.fulcrologic.fulcro.components :as comp :refer [defsc]] 6 | [com.fulcrologic.fulcro.dom :as dom])) 7 | 8 | (defsc SourceAnnotationDemo [this {:thing/keys [id] :as props}] 9 | {:query [:thing/id] 10 | :ident :thing/id 11 | :initial-state {:thing/id 1}} 12 | (let [m {} 13 | j #js {}] 14 | (dom/div 15 | (dom/p "nil") 16 | (dom/p :#paragraph "with css") 17 | (dom/div {} "map") 18 | (dom/div {:data-id id} "runtime-map") 19 | (dom/div m "symbol") 20 | (dom/div (merge m {}) "expression") 21 | (dom/div #js {} "js-object") 22 | (dom/div j "sym -> js-object") 23 | (dom/br) 24 | (dom/div (dom/div "one") (dom/div "one+")) 25 | (dom/div {} (dom/div "two")) 26 | (dom/div :.foo "three") 27 | (dom/div :.foo (dom/div "four")) 28 | (dom/div :.foo {} (dom/div "five")) 29 | ))) 30 | 31 | (ws/defcard source-annotation-demo-card 32 | (ct.fulcro/fulcro-card 33 | {::ct.fulcro/wrap-root? true 34 | ::ct.fulcro/root SourceAnnotationDemo})) 35 | -------------------------------------------------------------------------------- /src/workspaces/com/fulcrologic/fulcro/issues/issue431_computed_sync_tx_cards.cljs: -------------------------------------------------------------------------------- 1 | (ns com.fulcrologic.fulcro.issues.issue431-computed-sync-tx-cards 2 | (:require 3 | [nubank.workspaces.card-types.fulcro3 :as ct.fulcro] 4 | [nubank.workspaces.core :as ws] 5 | [com.fulcrologic.fulcro.dom :refer [div ul li p h3 button b p]] 6 | [com.fulcrologic.fulcro.components :as comp :refer [defsc]] 7 | [com.fulcrologic.fulcro.mutations :as m :refer [defmutation]] 8 | [com.fulcrologic.fulcro.algorithms.merge :as merge] 9 | [com.fulcrologic.fulcro.rendering.ident-optimized-render :as ior] 10 | [taoensso.timbre :as log])) 11 | 12 | (defsc Item 13 | [this {:item/keys [name]}] 14 | {:ident :item/id 15 | :query [:item/id :item/name :ui/clicked?] 16 | :initial-state {:item/id 1 :item/name "Item 1" :ui/clicked? false} 17 | :use-hooks? true} 18 | (let [computed-prop (comp/get-computed this :computed-prop)] 19 | (log/info :computed-prop computed-prop) 20 | (li 21 | (button {:onClick (fn [] 22 | (log/info "Computed prop from callback" computed-prop) 23 | #_(m/toggle!! this :ui/clicked?) 24 | ;; The single ! variation works 25 | (m/toggle! this :ui/clicked?))} 26 | name)))) 27 | 28 | (def ui-item (comp/factory Item {:keyfn :item/id})) 29 | 30 | (defsc Root [_this {:root/keys [items]}] 31 | {:query [{:root/items (comp/get-query Item)}] 32 | :initial-state {:root/items [{}]}} 33 | (div 34 | (p "Updating a child component that sets use-hooks? to true with transact!! makes it lose the computed props.") 35 | (p "Click on the button and see what happens to Item's computed props.") 36 | (ul 37 | (map #(ui-item (comp/computed % {:computed-prop :anything})) items)))) 38 | 39 | (ws/defcard form-pre-merge-sample 40 | (ct.fulcro/fulcro-card 41 | {::ct.fulcro/wrap-root? false 42 | ::ct.fulcro/root Root 43 | ::ct.fulcro/app {:optimized-render! ior/render!}})) 44 | -------------------------------------------------------------------------------- /src/workspaces/com/fulcrologic/fulcro/issues/pr552_string_buffered_input_cards.cljs: -------------------------------------------------------------------------------- 1 | (ns com.fulcrologic.fulcro.issues.pr552-string-buffered-input-cards 2 | (:require 3 | [com.fulcrologic.fulcro.algorithms.merge :as merge] 4 | [com.fulcrologic.fulcro.components :as comp :refer [defsc]] 5 | [com.fulcrologic.fulcro.dom :refer [b button div p h3 li p ul]] 6 | [com.fulcrologic.fulcro.dom.inputs :refer [StringBufferedInput]] 7 | [com.fulcrologic.fulcro.mutations :as m :refer [defmutation]] 8 | [com.fulcrologic.fulcro.rendering.ident-optimized-render :as ior] 9 | [nubank.workspaces.card-types.fulcro3 :as ct.fulcro] 10 | [nubank.workspaces.core :as ws])) 11 | 12 | (def MyInput (StringBufferedInput ::MyInput {:model->string identity 13 | :string->model identity})) 14 | 15 | (def ui-input (comp/factory MyInput)) 16 | 17 | (defsc Item 18 | [this {:item/keys [name]}] 19 | {:ident :item/id 20 | :query [:item/id :item/name :ui/clicked?] 21 | :initial-state {:item/id 1 :item/name "Item 1" :ui/clicked? false}} 22 | (let [v (last (for [n (range 1000000)] 23 | (+ n (* 2 n))))] 24 | (div 25 | (div (str v)) 26 | (ui-input {:value name 27 | :onChange (fn [v] (m/set-string! this :item/name :value v))}) 28 | (button {:onClick (fn [] (m/set-string! this :item/name :value "Bar"))} 29 | "Force to Bar") 30 | (button {:onClick (fn [] (m/set-string! this :item/name :value "Foo"))} 31 | "Force to Foo")))) 32 | 33 | (def ui-item (comp/factory Item {:keyfn :item/id})) 34 | 35 | (defsc Root [_this {:root/keys [item]}] 36 | {:query [{:root/item (comp/get-query Item)}] 37 | :initial-state {:root/item {}}} 38 | 39 | (div 40 | (ui-item item))) 41 | 42 | (ws/defcard string-buffered-input-card 43 | (ct.fulcro/fulcro-card 44 | {::ct.fulcro/wrap-root? false 45 | ::ct.fulcro/root Root})) 46 | -------------------------------------------------------------------------------- /tests.edn: -------------------------------------------------------------------------------- 1 | #kaocha/v1 2 | {:tests [{:id :unit 3 | :ns-patterns ["-test$" "-spec$"] 4 | :test-paths ["src/test"] 5 | :skip-meta [:integration] 6 | :source-paths ["src/main"]}] 7 | ;:reporter [fulcro-spec.reporters.terminal/fulcro-report] 8 | :plugins []} --------------------------------------------------------------------------------