├── .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 |
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 |
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 |
21 |
--------------------------------------------------------------------------------
/docs/assets/img/icon-complexity.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
23 |
--------------------------------------------------------------------------------
/docs/assets/img/icon-ff.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
33 |
--------------------------------------------------------------------------------
/docs/assets/img/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 |
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 |
45 |
--------------------------------------------------------------------------------
/docs/svg/query-demo-query-tree.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/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 []}
--------------------------------------------------------------------------------