├── src
├── test
│ ├── config
│ │ ├── defaults.edn
│ │ ├── other.edn
│ │ └── test.edn
│ └── com
│ │ └── fulcrologic
│ │ └── fulcro
│ │ ├── server
│ │ ├── dont_require_me.clj
│ │ └── config_spec.clj
│ │ ├── application_spec.cljc
│ │ ├── algorithms
│ │ ├── transit_spec.cljc
│ │ ├── indexing_spec.cljc
│ │ ├── data_targeting_spec.clj
│ │ └── normalize_spec.cljc
│ │ ├── routing
│ │ └── legacy_ui_routers_spec.cljc
│ │ ├── macros
│ │ ├── defmutation_spec.clj
│ │ └── defsc_spec.clj
│ │ ├── data_fetch_spec.cljc
│ │ ├── components_spec.cljc
│ │ └── dom_server_spec.clj
├── main
│ ├── data_readers.clj
│ └── com
│ │ └── fulcrologic
│ │ └── fulcro
│ │ ├── inspect
│ │ ├── preload.cljs
│ │ ├── transit.cljs
│ │ └── diff.cljc
│ │ ├── algorithms
│ │ ├── tx-processing-readme.adoc
│ │ ├── tx_processing_debug.cljc
│ │ ├── scheduling.cljc
│ │ ├── lookup.cljc
│ │ ├── transit.cljc
│ │ ├── tempid.cljc
│ │ ├── timbre_support.cljs
│ │ ├── do_not_use.cljc
│ │ ├── normalize.cljc
│ │ ├── indexing.cljc
│ │ └── data_targeting.cljc
│ │ ├── rendering
│ │ ├── keyframe_render.cljc
│ │ └── ident_optimized_render.cljc
│ │ ├── networking
│ │ └── mock_server_remote.cljs
│ │ ├── dom
│ │ ├── events.cljc
│ │ └── html_entities.cljc
│ │ ├── server
│ │ ├── config.clj
│ │ └── api_middleware.clj
│ │ ├── dom_common.cljc
│ │ └── dom.clj
├── todomvc
│ └── fulcro_todomvc
│ │ ├── main.cljs
│ │ ├── websocket_server.clj
│ │ ├── server.cljs
│ │ ├── api.cljs
│ │ ├── server.clj
│ │ └── ui_with_legacy_ui_routers.cljs
└── dev
│ └── user.clj
├── .github
└── FUNDING.yml
├── docs
└── logo.png
├── Makefile
├── README.adoc
├── ghostwheel.edn
├── tests.edn
├── karma.conf.js
├── resources
└── public
│ ├── todo.html
│ ├── base.css
│ └── index.css
├── package.json
├── LICENSE
├── .circleci
└── config.yml
├── .gitignore
├── CONTRIBUTING.md
├── shadow-cljs.edn
├── deps.edn
├── pom.xml
└── CHANGELOG.md
/src/test/config/defaults.edn:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | patreon: fulcro
2 |
--------------------------------------------------------------------------------
/src/main/data_readers.clj:
--------------------------------------------------------------------------------
1 | {js clojure.core/identity}
2 |
--------------------------------------------------------------------------------
/src/test/config/other.edn:
--------------------------------------------------------------------------------
1 | {:some-key :some-default-val}
--------------------------------------------------------------------------------
/docs/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fulcro-legacy/fulcro3/HEAD/docs/logo.png
--------------------------------------------------------------------------------
/src/test/com/fulcrologic/fulcro/server/dont_require_me.clj:
--------------------------------------------------------------------------------
1 | (ns com.fulcrologic.fulcro.server.dont-require-me)
2 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | tests:
2 | npm install
3 | npx shadow-cljs -A:dev compile ci-tests
4 | npx karma start --single-run
5 | clojure -A:dev:test:clj-tests
6 |
7 | dev:
8 | clojure -A:dev:test:clj-tests -J-Dghostwheel.enabled=true --watch --fail-fast --no-capture-output
9 |
--------------------------------------------------------------------------------
/src/todomvc/fulcro_todomvc/main.cljs:
--------------------------------------------------------------------------------
1 | (ns fulcro-todomvc.main
2 | (:require
3 | [com.fulcrologic.fulcro.networking.websockets :as fws]
4 | [com.fulcrologic.fulcro.application :as app]))
5 |
6 | (defonce app (app/fulcro-app {:remotes {:remote (fws/fulcro-websocket-remote {})}}))
7 |
--------------------------------------------------------------------------------
/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 | This was a work-in-progress repository. Fulcro 3 has moved to the mail Fulcro repository.
12 |
--------------------------------------------------------------------------------
/src/main/com/fulcrologic/fulcro/inspect/preload.cljs:
--------------------------------------------------------------------------------
1 | (ns com.fulcrologic.fulcro.inspect.preload
2 | "Namespace to use in your compiler preload in order to enable inspect support during development."
3 | (:require
4 | [com.fulcrologic.fulcro.inspect.inspect-client :as inspect]))
5 |
6 | (inspect/install {})
7 |
--------------------------------------------------------------------------------
/ghostwheel.edn:
--------------------------------------------------------------------------------
1 | {:trace 0
2 | :trace-color :violet
3 | :check false
4 | :check-coverage false
5 | :ignore-fx false
6 | :num-tests 5
7 | :num-tests-ext 0
8 | :extensive-tests false
9 | :defn-macro nil
10 | :instrument true
11 | :outstrument false
12 | :extrument nil
13 | :expound {:show-valid-values? true
14 | :print-specs? true}
15 | :report-output :js-console}
--------------------------------------------------------------------------------
/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 [:kaocha.plugin/randomize
9 | :kaocha.plugin/filter
10 | :kaocha.plugin/capture-output]}
--------------------------------------------------------------------------------
/src/dev/user.clj:
--------------------------------------------------------------------------------
1 | (ns user
2 | (:require
3 | [clojure.pprint :refer [pprint]]
4 | [clojure.test :refer :all]
5 | [clojure.repl :refer [doc source]]
6 | [clojure.tools.namespace.repl :as tools-ns :refer [disable-reload! refresh clear set-refresh-dirs]]
7 | [expound.alpha :as expound]
8 | [clojure.spec.alpha :as s]
9 | [edn-query-language.core :as eql]))
10 |
11 | (set-refresh-dirs "src/main" "src/test" "src/dev" "src/tutorial" "src/cards")
12 | (alter-var-root #'s/*explain-out* (constantly expound/printer))
13 |
14 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/resources/public/todo.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
TodoMVC
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/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 | [com.fulcrologic.fulcro.specs]
6 | [com.fulcrologic.fulcro.algorithms.do-not-use :as util]
7 | [com.fulcrologic.fulcro.application :as app :refer [fulcro-app]]
8 | [clojure.test :refer [is are deftest]]))
9 |
10 | (deftest application-constructor
11 | (let [app (app/fulcro-app)]
12 | (assertions
13 | (s/valid? ::app/app app) => true)))
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fulcro",
3 | "version": "1.0.0",
4 | "description": "Testing",
5 | "main": "index.js",
6 | "directories": {},
7 | "devDependencies": {
8 | "karma": "3.1.4",
9 | "karma-chrome-launcher": "2.2.0",
10 | "karma-cljs-test": "0.1.0",
11 | "react": "16.8.6",
12 | "react-dom": "16.8.6",
13 | "shadow-cljs": "2.8.40"
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "git@github.com:fulcrologic/fulcro3.git"
18 | },
19 | "author": "",
20 | "license": "MIT",
21 | "dependencies": {
22 | "codemirror": "^5.47.0",
23 | "d3": "^5.9.2",
24 | "highlight.js": "^9.15.8",
25 | "parinfer": "^3.12.0",
26 | "parinfer-codemirror": "^1.4.2",
27 | "react-grid-layout": "^0.16.6",
28 | "react-icons": "^2.2.7",
29 | "reakit": "^0.11.2"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/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/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 t]
4 | [clojure.test :refer [are]]
5 | [fulcro-spec.core :refer [specification assertions]]))
6 |
7 | (specification "transit-clj->str and str->clj"
8 | (assertions
9 | "Encode clojure data structures to strings"
10 | (string? (t/transit-clj->str {})) => true
11 | (string? (t/transit-clj->str [])) => true
12 | (string? (t/transit-clj->str 1)) => true
13 | (string? (t/transit-clj->str 22M)) => true
14 | (string? (t/transit-clj->str #{1 2 3})) => true
15 | "Can decode encodings"
16 | (t/transit-str->clj (t/transit-clj->str {:a 1})) => {:a 1}
17 | (t/transit-str->clj (t/transit-clj->str #{:a 1})) => #{:a 1}
18 | (t/transit-str->clj (t/transit-clj->str "Hi")) => "Hi"))
19 |
--------------------------------------------------------------------------------
/src/main/com/fulcrologic/fulcro/inspect/transit.cljs:
--------------------------------------------------------------------------------
1 | (ns 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 |
6 | (deftype ErrorHandler []
7 | Object
8 | (tag [this v] "js-error")
9 | (rep [this v] [(ex-message v) (ex-data v)])
10 | (stringRep [this v] (ex-message v)))
11 |
12 | (deftype DefaultHandler []
13 | Object
14 | (tag [this v] "unknown")
15 | (rep [this v] (pr-str v)))
16 |
17 | (def write-handlers
18 | {cljs.core/ExceptionInfo (ErrorHandler.)
19 | "default" (DefaultHandler.)})
20 |
21 | (def read-handlers
22 | {"js-error" (fn [[msg data]] (ex-info msg data))})
23 |
24 | (defn read [str]
25 | (let [reader (ft/reader {:handlers read-handlers})]
26 | (t/read reader str)))
27 |
28 | (defn write [x]
29 | (let [writer (ft/writer {:handlers write-handlers})]
30 | (t/write writer x)))
31 |
32 | (extend-type ty/UUID IUUID)
33 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | jobs:
3 | cljs:
4 | docker:
5 | - image: circleci/clojure:openjdk-8-tools-deps-node-browsers
6 | steps:
7 | - checkout
8 | - restore_cache:
9 | key: cljs-{{ checksum "deps.edn" }}-{{ checksum "package.json" }}
10 | - run: npm install
11 | - run: npx shadow-cljs -A:dev -v compile ci-tests
12 | - run: ls -l target
13 | - run: npx karma start --single-run
14 | - save_cache:
15 | paths:
16 | - node_modules
17 | - ~/.m2
18 | key: cljs-{{ checksum "deps.edn" }}-{{ checksum "package.json" }}
19 | clj:
20 | docker:
21 | - image: circleci/clojure:tools-deps-1.9.0.394
22 | steps:
23 | - checkout
24 | - restore_cache:
25 | key: clj-{{ checksum "deps.edn" }}
26 | - run: clojure -A:dev:test:clj-tests -J-Dghostwheel.enabled=true
27 | - save_cache:
28 | paths:
29 | - ~/.m2
30 | key: clj-{{ checksum "deps.edn" }}
31 | workflows:
32 | version: 2
33 | fullstack:
34 | jobs:
35 | - clj
36 | - cljs
37 |
--------------------------------------------------------------------------------
/.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 | pom.xml.asc
35 | resources/private/js
36 | resources/public/js
37 | resources/public/js/cards
38 | resources/public/js/test
39 | target
40 | resources/public/getting-started.html
41 | resources/public/.asciidoctor/
42 | resources/public/*.png
43 | package-lock.json
44 | docs/_site
45 | docs/.sass-cache
46 | docs/Gemfile.lock
47 | docs/js/[a-fh-su-z]*
48 | docs/js/goog
49 | docs/js/garden
50 | old-docs/.asciidoctor
51 | old-docs/plumbing.png
52 | DevelopersGuide.html
53 | docs/.jekyll-metadata
54 | .floo
55 | .flooignore
56 | .asciidoctor
57 | /ReferenceGuide.html
58 | .cpcache
59 | .shadow-cljs
60 | resources/public/workspaces
61 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | If you'd like to submit a PR, please follow these general guidelines:
4 |
5 | - Read through the current issues on Github. There might be something you can
6 | help with!
7 | - Either talk about it in Slack on #fulcro, or open a github issue
8 | - Please use a github issue to go along with your PR.
9 | - Do development against the *develop* branch (we use git flow). PRs should be directed at the develop branch. Master is
10 | the latest release, not the live development.
11 | - In general, please squash your change into a single commit
12 | - Add an entry to the CHANGELOG describing the change
13 |
14 | ## Git Flow on OSX
15 |
16 | Please read about [Git Flow](http://nvie.com/posts/a-successful-git-branching-model/)
17 |
18 | I use homebrew to install git flow extensions:
19 |
20 | ```bash
21 | $ brew install git-flow-avh
22 | ```
23 |
24 | and make sure my startup shell file (I use .bashrc) sources the completion file:
25 |
26 | ```
27 | . /usr/local/etc/bash_completion.d/git-flow-completion.bash
28 | ```
29 |
30 | There is also a plain `git-flow` package, but the AVH version is better maintained and has better hook support.
31 |
32 | ## Github Instructions
33 |
34 | Basically follow the instructions here:
35 |
36 | https://help.github.com/categories/collaborating-with-issues-and-pull-requests/
37 |
38 | ## General Guidelines
39 |
40 | I need to rewrite these for this project. Ping me on Slack.
41 |
--------------------------------------------------------------------------------
/shadow-cljs.edn:
--------------------------------------------------------------------------------
1 | {:deps {:aliases [:dev :test]}
2 | :nrepl {:port 9000}
3 | :dev-http {9001 "resources/public"}
4 | :jvm-opts ["-Xmx2G" #_"-Dghostwheel.enabled=true"]
5 | :builds {:todomvc {:target :browser
6 | :output-dir "resources/public/js/todomvc"
7 | :asset-path "/js/todomvc"
8 | :dev {:compiler-options {:external-config {:ghostwheel {}}}}
9 | :modules {:main {:entries [fulcro-todomvc.main]}}
10 | :devtools {:preloads [com.fulcrologic.fulcro.inspect.preload]}}
11 |
12 | :test {:target :browser-test
13 | :test-dir "resources/public/js/test"
14 | :ns-regexp "-spec$"
15 | :dev {:compiler-options {:external-config {:ghostwheel {}}}}
16 | :compiler-options {:static-fns false} ; required for mocking to work
17 | :devtools {:http-port 9002
18 | :http-resource-root "public"
19 | :http-root "resources/public/js/test"}}
20 |
21 | :ci-tests {:target :karma
22 | :js-options {:js-provider :shadow}
23 | :compiler-options {:static-fns false} ; required for mocking to work
24 | :output-to "target/ci.js"
25 | :ns-regexp "-spec$"}}}
26 |
27 |
--------------------------------------------------------------------------------
/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
4 | top-level transaction. Loads use an `internal-load` mutation, so they are always a single top-level tx.
5 |
6 |
7 | To Do:
8 |
9 | - There should be a function that takes the nodes with the same ID that are queued and re-combines them. This is
10 | the current combine-sends, but the logic needs to not do reordering or even return what to send.
11 | - The next step of processing the send queue should be optional write reordering
12 | - The next step should then be potentially creating a multi-send send node so that the entire queue can be sent on
13 | one network request.
14 |
15 |
16 | So, desired send processing:
17 |
18 | 1. restore transaction semantics: fn of send-queue -> send-queue. Combines send nodes with a common tx ID into a single send node so
19 | that as much of the original tx will be sent as a unit as is possible.
20 | 2. Reorder the queue: Pluggable and optional. Put writes first, enable customization like "tx priorities"
21 | 3. Merge the queue (optional, requires enabling on client and server): For a given remote it is possible for us to encode
22 | a "multi-send", where the send nodes from (1/2) are combined into a data structure that allows the lower-level
23 | networking to send the entire queue in one network round-trip. Something like a vector of maps?
24 | `[{::id id ::tx tx} {::id id2 ::tx tx}]`, where the server returns `{id result id2 result ...}`.
--------------------------------------------------------------------------------
/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 | [immutant.web :as web]
6 | [ring.middleware.content-type :refer [wrap-content-type]]
7 | [ring.middleware.not-modified :refer [wrap-not-modified]]
8 | [ring.middleware.resource :refer [wrap-resource]]
9 | [ring.middleware.params :refer [wrap-params]]
10 | [ring.middleware.keyword-params :refer [wrap-keyword-params]]
11 | [ring.util.response :refer [response file-response resource-response]]
12 | [taoensso.sente.server-adapters.immutant :refer [get-sch-adapter]]))
13 |
14 | (def server (atom nil))
15 |
16 | (defn query-parser
17 | ""
18 | [query]
19 | ;; call out to something like a pathom parser. See Fulcro Developers Guide
20 | )
21 |
22 | (defn http-server []
23 | (let [websockets (fws/start! (fws/make-websockets
24 | query-parser
25 | {:http-server-adapter (get-sch-adapter)
26 | ;; See Sente for CSRF instructions
27 | :sente-options {:csrf-token-fn nil}}))
28 | middleware (-> not-found-handler
29 | (fws/wrap-api websockets)
30 | wrap-keyword-params
31 | wrap-params
32 | (wrap-resource "public")
33 | wrap-content-type
34 | wrap-not-modified)
35 | result (web/run middleware {:host "0.0.0.0"
36 | :port 3000})]
37 | (reset! server
38 | (fn []
39 | (fws/stop! websockets)
40 | (web/stop result)))))
41 |
42 | (comment
43 |
44 | ;; start
45 | (http-server)
46 |
47 | ;; stop
48 | (@server))
49 |
--------------------------------------------------------------------------------
/src/main/com/fulcrologic/fulcro/inspect/diff.cljc:
--------------------------------------------------------------------------------
1 | (ns com.fulcrologic.fulcro.inspect.diff
2 | "Internal algorithms for sending db diffs to Inspect tool."
3 | (:require [clojure.spec.alpha :as s]))
4 |
5 | (defn updates [a b]
6 | (reduce
7 | (fn [adds [k v]]
8 | (let [va (get a k :fulcro.inspect.lib.diff/unset)]
9 | (if (= v va)
10 | adds
11 | (if (and (map? v) (map? va))
12 | (assoc adds k (updates va v))
13 | (assoc adds k v)))))
14 | {}
15 | b))
16 |
17 | (defn removals [a b]
18 | (reduce
19 | (fn [rems [k v]]
20 | (if-let [[_ vb] (find b k)]
21 | (if (and (map? v) (map? vb) (not= v vb))
22 | (let [childs (removals v vb)]
23 | (if (seq childs)
24 | (conj rems {k childs})
25 | rems))
26 | rems)
27 | (conj rems (cond-> k (map? k) (assoc :fulcro.inspect.lib.diff/key? true)))))
28 | []
29 | a))
30 |
31 | (defn diff [a b]
32 | {:fulcro.inspect.lib.diff/updates (updates a b)
33 | :fulcro.inspect.lib.diff/removals (removals a b)})
34 |
35 | (defn deep-merge [x y]
36 | (if (and (map? x) (map? y))
37 | (merge-with deep-merge x y)
38 | y))
39 |
40 | (defn patch-updates [x {:fulcro.inspect.lib.diff/keys [updates]}]
41 | (merge-with deep-merge x updates))
42 |
43 | (defn patch-removals [x {:fulcro.inspect.lib.diff/keys [removals]}]
44 | (reduce
45 | (fn [final rem]
46 | (cond
47 | (:fulcro.inspect.lib.diff/key? rem)
48 | (dissoc final (dissoc rem :fulcro.inspect.lib.diff/key?))
49 |
50 | (map? rem)
51 | (let [[k v] (first rem)]
52 | (update final k #(patch-removals % {:fulcro.inspect.lib.diff/removals v})))
53 |
54 | :else
55 | (dissoc final rem)))
56 | x
57 | removals))
58 |
59 | (defn patch [x diff]
60 | (-> x
61 | (patch-updates diff)
62 | (patch-removals diff)))
63 |
--------------------------------------------------------------------------------
/deps.edn:
--------------------------------------------------------------------------------
1 | {:paths ["src/main" "src/todomvc"]
2 |
3 | :deps {edn-query-language/eql {:mvn/version "0.0.8"}
4 | cljsjs/react {:mvn/version "16.8.6-0"}
5 | cljsjs/react-dom {:mvn/version "16.8.6-0"}
6 | cljsjs/react-dom-server {:mvn/version "16.8.6-0"}
7 | com.taoensso/timbre {:mvn/version "4.10.0"}
8 | com.taoensso/encore {:mvn/version "2.115.0"}
9 | com.taoensso/tufte {:mvn/version "2.0.1"}
10 | com.cognitect/transit-clj {:mvn/version "0.8.313"}
11 | com.cognitect/transit-cljs {:mvn/version "0.8.256"}
12 | com.wsscode/pathom {:mvn/version "2.2.14"}
13 | gnl/ghostwheel {:mvn/version "0.3.9"}
14 | org.clojure/clojure {:mvn/version "1.10.0"}
15 | org.clojure/clojurescript {:mvn/version "1.10.520"}
16 | org.clojure/core.async {:mvn/version "0.4.474"}}
17 |
18 | :aliases {:test {:extra-paths ["src/test"]
19 | :extra-deps {org.clojure/test.check {:mvn/version "0.9.0"}
20 | fulcrologic/fulcro-spec {:mvn/version "3.1.4"}}}
21 |
22 | :clj-tests {:extra-paths ["src/test"]
23 | :main-opts ["-m" "kaocha.runner"]
24 | :extra-deps {lambdaisland/kaocha {:mvn/version "0.0-529"}}}
25 |
26 | :dev {:extra-paths ["src/dev" "resources"]
27 | :extra-deps {thheller/shadow-cljs {:mvn/version "2.8.40"}
28 | com.fulcrologic/fulcro-websockets {:mvn/version "3.0.1"}
29 | binaryage/devtools {:mvn/version "0.9.10"}
30 | ring/ring-core {:mvn/version "1.7.1"}
31 | org.immutant/web {:mvn/version "2.1.10"}
32 | org.clojure/tools.namespace {:mvn/version "0.3.0-alpha4"}}}}}
33 |
--------------------------------------------------------------------------------
/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 | [com.fulcrologic.fulcro.algorithms.denormalize :as fdn]
5 | [com.fulcrologic.fulcro.algorithms.lookup :as ah]
6 | [com.fulcrologic.fulcro.components :as comp]))
7 |
8 | (defn render!
9 | "Render the UI. The keyframe render runs a full UI query and then asks React to render the root component.
10 | The optimizations for this kind of render are purely those provided by `defsc`'s default
11 | shouldComponentUpdate, which causes component to act like React PureComponent (though the props compare in cljs
12 | is often faster).
13 |
14 | If `:hydrate?` is true it will use the React hydrate functionality (on browsers) to render over
15 | server-rendered content in the DOM.
16 |
17 | If `:force-root? true` is included in the options map then not only will this do a keyframe update, it will also
18 | force all components to return `false` from `shouldComponentUpdate`."
19 | [app {:keys [force-root? hydrate?] :as options}]
20 | (binding [comp/*blindly-render* force-root?]
21 | (let [{:com.fulcrologic.fulcro.application/keys [runtime-atom state-atom]} app
22 | {:com.fulcrologic.fulcro.application/keys [root-factory root-class mount-node]} @runtime-atom
23 | r! (if hydrate?
24 | (or (ah/app-algorithm app :hydrate-root!) #?(:cljs js/ReactDOM.hydrate) #?(:cljs js/ReactDOM.render))
25 | (or (ah/app-algorithm app :render-root!) #?(:cljs js/ReactDOM.render)))
26 | state-map @state-atom
27 | query (comp/get-query root-class state-map)
28 | data-tree (if query
29 | (fdn/db->tree query state-map state-map)
30 | state-map)
31 | app-root #?(:clj {}
32 | :cljs (r! (root-factory data-tree) mount-node))]
33 | (swap! runtime-atom assoc :com.fulcrologic.fulcro.application/app-root app-root)
34 | #?(:cljs app-root))))
35 |
--------------------------------------------------------------------------------
/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."))))
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."))))]
28 | (try
29 | (async/go
30 | (let [result (async/ k name) " " (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? :com.fulcrologic.fulcro.algorithms.tx-processing/complete? :com.fulcrologic.fulcro.algorithms.tx-processing/original-ast-node))
20 | (println " Dispatch: " (with-out-str (pprint dispatch)))
21 | (println " Results: " (with-out-str (pprint results)))))
22 | prsend (fn [s]
23 | (println "NODE:")
24 | (println " " (strks s :com.fulcrologic.fulcro.algorithms.tx-processing/ast :com.fulcrologic.fulcro.algorithms.tx-processing/active?)))]
25 | (println "================================================================================")
26 | (println "Submission Queue:")
27 | (doseq [n submission-queue]
28 | (prtxnode n))
29 | (println "Active Queue:")
30 | (doseq [n active-queue]
31 | (prtxnode n))
32 | (println "Send Queues:")
33 | (doseq [k (keys send-queues)]
34 | (println k " Send Queue:")
35 | (doseq [n (get send-queues k)]
36 | (prsend n)))
37 | (println "================================================================================")))
38 |
--------------------------------------------------------------------------------
/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 | [ghostwheel.core :refer [>fdef =>]]
5 | [clojure.core.async :as async]))
6 |
7 | (defn defer
8 | "Schedule f to run in `tm` ms."
9 | [f tm]
10 | #?(:cljs (js/setTimeout f tm)
11 | :clj (async/go
12 | (async/ any?]
24 | (let [{:com.fulcrologic.fulcro.application/keys [runtime-atom]} app]
25 | (when-not (get @runtime-atom scheduled-key)
26 | (swap! runtime-atom assoc scheduled-key true)
27 | (defer (fn []
28 | (swap! runtime-atom assoc scheduled-key false)
29 | (action app)) tm))))
30 | ([app scheduled-key action]
31 | [:com.fulcrologic.fulcro.application/app keyword? fn? => any?]
32 | (schedule! app scheduled-key action 0)))
33 |
34 | (defn schedule-animation!
35 | "Schedule the processing of a specific action in the runtime atom on the next animation frame.
36 |
37 | - `scheduled-key` - The runtime flag that tracks scheduling for the processing.
38 | - `action` - The function to run when the scheduled time comes."
39 | ([app scheduled-key action]
40 | [:com.fulcrologic.fulcro.application/app keyword? fn? => any?]
41 | #?(:clj (action)
42 | :cljs (let [{:com.fulcrologic.fulcro.application/keys [runtime-atom]} app]
43 | (when-not (get @runtime-atom scheduled-key)
44 | (swap! runtime-atom assoc scheduled-key true)
45 | (let [f (fn []
46 | (swap! runtime-atom assoc scheduled-key false)
47 | (action))]
48 | (if-not (exists? js/requestAnimationFrame)
49 | (defer f 16)
50 | (js/requestAnimationFrame f))))))))
51 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/main/com/fulcrologic/fulcro/algorithms/lookup.cljc:
--------------------------------------------------------------------------------
1 | (ns com.fulcrologic.fulcro.algorithms.lookup
2 | "Namespace with support for finding plug-in algorithms on the app. Avoids circular references
3 | within the library itself."
4 | (:require
5 | [taoensso.timbre :as log]))
6 |
7 | (defn app-algorithm
8 | "Get the current value of a particular Fulcro plugin algorithm. These are set by default and can be overridden
9 | when you create your fulcro app.
10 |
11 | `app` - The application
12 | `k` - the algorithm to obtain. This can be a plain keyword or a symbol of the algorithm desired.
13 |
14 | Supported algorithms that can be obtained/overridden in Fulcro (check the source of app/fulcro-app if you suspect this is out
15 | of date):
16 |
17 | - `:tx!` - Internal implementation of transaction submission. Default `app/default-tx!`
18 | - `:global-eql-transform` - A `(fn [tx] tx')` that is applied to all outgoing requests (when using default `tx!`).
19 | Defaults to stripping things like `:ui/*` and form state config joins.
20 | - `:remote-error?` - A `(fn [result] boolean)` that defines what a remote error is.
21 | - `:global-error-action` - A `(fn [env] ...)` that is run on any remote error (as defined by `remote-error?`).
22 | - `:optimized-render!` - The concrete render algorithm for optimized renders (not root refreshes)
23 | - `:render!` - The top-level render function. Calls root render or optimized render by default. Renders on the calling thread.
24 | - `:schedule-render!` - The call that schedules a render. Defaults to using `js/requestAnimationFrame`.
25 | - `:default-result-action!` - The action used for remote results in all mutations that do not have a `result-action` section.
26 | - `:index-root!` - The algorithm that scans the current query from root an indexes all classes by their queries.
27 | - `:index-component!` - The algorithm that adds a component to indexes when it mounts.
28 | - `:drop-component!` - The algorithm that removes a component from indexes when it unmounts.
29 | - `:props-middleware` - Middleware that can modify `props` for all components.
30 | - `:render-middleware` - Middlware that wraps all `render` methods of `defsc` components.
31 |
32 | Returns nil if the algorithm is currently undefined.
33 | "
34 | [{:com.fulcrologic.fulcro.application/keys [algorithms] :as app} k]
35 | (when-let [nm (when (or (string? k) (keyword? k) (symbol? k))
36 | (keyword "com.fulcrologic.fulcro.algorithm" (name k)))]
37 | (when-not (contains? algorithms nm)
38 | (log/warn "Attempt to access an undefined app algorithm" k))
39 | (get-in app [:com.fulcrologic.fulcro.application/algorithms nm] nil)))
40 |
--------------------------------------------------------------------------------
/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/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 |
8 | (declare =>)
9 |
10 | (specification "defmutation Macro"
11 | (component "Defining a mutation into a different namespace"
12 | (let [actual (m/defmutation* {}
13 | '(other/boo [params]
14 | (action [env] (swap! state))))]
15 | (assertions
16 | "Emits a defmethod with the proper symbol, action, and default result action."
17 | actual => `(defmethod com.fulcrologic.fulcro.mutations/mutate 'other/boo [~'fulcro-mutation-env-symbol]
18 | (let [~'params (-> ~'fulcro-mutation-env-symbol :ast :params)]
19 | {:result-action (fn [~'env]
20 | (when-let [~'default-action (ah/app-algorithm (:app ~'env) :default-result-action!)]
21 | (~'default-action ~'env)))
22 | :action (fn ~'action [~'env] (~'swap! ~'state) nil)})))))
23 | (component "Overridden result action"
24 | (let [actual (m/defmutation* {}
25 | '(other/boo [params]
26 | (result-action [env] (print "Hi"))))]
27 | (assertions
28 | "Uses the user-supplied version of default action"
29 | actual => `(defmethod com.fulcrologic.fulcro.mutations/mutate 'other/boo [~'fulcro-mutation-env-symbol]
30 | (let [~'params (-> ~'fulcro-mutation-env-symbol :ast :params)]
31 | {:result-action (fn ~'result-action [~'env] (~'print "Hi") nil)})))))
32 | (component "Mutation remotes"
33 | (let [actual (m/defmutation* {}
34 | '(boo [params]
35 | (action [env] (swap! state))
36 | (remote [env] true)
37 | (rest [env] true)))
38 | method (nth actual 2)
39 | body (nth method 4)]
40 | (assertions
41 | "Converts all sections to lambdas of a defmethod"
42 | (first method) => `defmethod
43 | body => `(let [~'params (-> ~'fulcro-mutation-env-symbol :ast :params)]
44 | {:result-action (fn [~'env]
45 | (when-let [~'default-action (ah/app-algorithm (:app ~'env) :default-result-action!)]
46 | (~'default-action ~'env)))
47 | :remote (fn ~'remote [~'env] true)
48 | :rest (fn ~'rest [~'env] true)
49 | :action (fn ~'action [~'env] (~'swap! ~'state) nil)})))))
50 |
--------------------------------------------------------------------------------
/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 | (def app (app/fulcro-app))
34 |
35 | (specification "Load parameters"
36 | (let [
37 | query-with-params (:query (df/load-params* app :prop Person {:params {:n 1}}))
38 | ident-query-with-params (:query (df/load-params* app [:person/by-id 1] Person {:params {:n 1}}))]
39 | (assertions
40 | "Accepts nil for subquery and params"
41 | (:query (df/load-params* app [:person/by-id 1] nil {})) => [[:person/by-id 1]]
42 | "Constructs query with parameters when subquery is nil"
43 | (:query (df/load-params* app [:person/by-id 1] nil {:params {:x 1}})) => '[([:person/by-id 1] {:x 1})]
44 | "Constructs a JOIN query (without params)"
45 | (:query (df/load-params* app :prop Person {})) => [{:prop (comp/get-query Person)}]
46 | (:query (df/load-params* app [:person/by-id 1] Person {})) => [{[:person/by-id 1] (comp/get-query Person)}]
47 | "Honors target for property-based join"
48 | (:target (df/load-params* app :prop Person {:target [:a :b]})) => [:a :b]
49 | "Constructs a JOIN query (with params on join and prop)"
50 | query-with-params => `[({:prop ~(comp/get-query Person)} {:n 1})]
51 | ident-query-with-params => `[({[:person/by-id 1] ~(comp/get-query Person)} {:n 1})]))
52 | (behavior "can focus the query"
53 | (assertions
54 | (:query (df/load-params* app [:item/by-id 1] Item {:focus [:name {:comments [:title]}]}))
55 | => [{[:item/by-id 1] [:name {:comments [:title]}]}]))
56 | (behavior "can update the query with custom processing"
57 | (assertions
58 | (:query (df/load-params* app [:item/by-id 1] Item {:focus [:name]
59 | :update-query #(conj % :extra)}))
60 | => [{[:item/by-id 1] [:name :extra]}])))
61 |
--------------------------------------------------------------------------------
/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 | #?(:cljs [goog.object :as gobj])
8 | [fulcro-spec.core :refer [specification assertions behavior component]]))
9 |
10 | (defsc A [this props]
11 | {:ident :person/id
12 | :query [:person/id :person/name]
13 | :initial-state {:person/id 1 :person/name "Tony"}
14 | :extra-data 42}
15 | (dom/div "TODO"))
16 |
17 | (defsc B [this props]
18 | {:ident :person/id
19 | :query [:person/id :person/name]
20 | :initial-state (fn [{:keys [id]}] {:person/id id :person/name "Tony"})
21 | :extra-data 42}
22 | (dom/div "TODO"))
23 |
24 | (def ui-a (comp/factory A))
25 |
26 | (specification "Component basics"
27 | (let [ui-a (comp/factory A)]
28 | (assertions
29 | "Supports arbitrary option data"
30 | (-> A comp/component-options :extra-data) => 42
31 | "Component class can be detected"
32 | (comp/component-class? A) => true
33 | (comp/component-class? (ui-a {})) => false
34 | "The component name is available from the class"
35 | (comp/component-name A) => (str `A)
36 | "The registry key is available from the class"
37 | (comp/class->registry-key A) => ::A
38 | "Can be used to obtain the ident"
39 | (comp/get-ident A {:person/id 4}) => [:person/id 4]
40 | "Can be used to obtain the query"
41 | (comp/get-query A) => [:person/id :person/name]
42 | "Initial state"
43 | (comp/get-initial-state A) => {:person/name "Tony" :person/id 1}
44 | (comp/get-initial-state B {:id 22}) => {:person/name "Tony" :person/id 22}
45 | "The class is available in the registry using a symbol or keyword"
46 | (comp/registry-key->class ::A) => A
47 | (comp/registry-key->class `A) => A)))
48 |
49 | (specification "computed props"
50 | (assertions
51 | "Can be added and extracted on map-based props"
52 | (comp/get-computed (comp/computed {} {:x 1})) => {:x 1}
53 | "Can be added and extracted on vector props"
54 | (comp/get-computed (comp/computed [] {:x 1})) => {:x 1}))
55 |
56 | (specification "newer-props"
57 | (assertions
58 | "Returns the first props if neither are timestamped"
59 | (comp/newer-props {:a 1} {:a 2}) => {:a 1}
60 | "Returns the second props if the first are nil"
61 | (comp/newer-props nil {:a 2}) => {:a 2}
62 | "Returns the newer props if both have times"
63 | (comp/newer-props (fdn/with-time {:a 1} 1) (fdn/with-time {:a 2} 2)) => {:a 2}
64 | (comp/newer-props (fdn/with-time {:a 1} 2) (fdn/with-time {:a 2} 1)) => {:a 1}
65 | "Returns the second props if the times are the same"
66 | (comp/newer-props (fdn/with-time {:a 1} 1) (fdn/with-time {:a 2} 1)) => {:a 2}))
67 |
68 | (specification "classname->class"
69 | (assertions
70 | "Returns from registry under fq keyword"
71 | (nil? (comp/registry-key->class ::A)) => false
72 | "Returns from registry under fq symbol"
73 | (nil? (comp/registry-key->class `A)) => false))
74 |
75 | (specification "react-type"
76 | (assertions
77 | "Returns the class when passed an instance"
78 | #?(:clj (comp/react-type (ui-a {}))
79 | :cljs (comp/react-type (A.))) => A))
80 |
81 | (specification "wrap-update-extra-props"
82 | (let [wrapper (comp/wrap-update-extra-props (fn [_ p] (assoc p :X 1)))
83 | updated-props (wrapper A #js {})]
84 | (assertions
85 | "Places extra props in raw props at :fulcro$extra_props"
86 | (comp/isoget updated-props :fulcro$extra_props) => {:X 1})))
87 |
--------------------------------------------------------------------------------
/src/main/com/fulcrologic/fulcro/algorithms/transit.cljc:
--------------------------------------------------------------------------------
1 | (ns com.fulcrologic.fulcro.algorithms.transit
2 | "Transit functions for the on-the-wire EDN communication to common remotes. Includes support for Fulcro tempids,
3 | and can be extended to support additional application-specific data types."
4 | #?(:clj
5 | (:refer-clojure :exclude [ref]))
6 | (:require [cognitect.transit :as t]
7 | #?(:cljs [com.cognitect.transit :as ct])
8 | [com.fulcrologic.fulcro.algorithms.tempid :as tempid #?@(:cljs [:refer [TempId]])])
9 | #?(:clj
10 | (:import [com.cognitect.transit
11 | TransitFactory WriteHandler ReadHandler]
12 | [com.fulcrologic.fulcro.algorithms.tempid TempId])))
13 |
14 | #?(:cljs
15 | (deftype TempIdHandler []
16 | Object
17 | (tag [_ _] tempid/tag)
18 | (rep [_ r] (. r -id))
19 | (stringRep [_ _] nil)))
20 |
21 | #?(:clj
22 | (deftype TempIdHandler []
23 | WriteHandler
24 | (tag [_ _] tempid/tag)
25 | (rep [_ r] (.-id ^TempId r))
26 | (stringRep [_ r] (str tempid/tag "#" r))
27 | (getVerboseHandler [_] nil)))
28 |
29 | #?(:cljs
30 | (defn writer
31 | "Create a transit writer.
32 |
33 | - `out`: An acceptable output for transit writers.
34 | - `opts`: (optional) options to pass to `cognitect.transit/writer` (such as handlers)."
35 | ([]
36 | (writer {}))
37 | ([opts]
38 | (t/writer :json
39 | (assoc-in opts [:handlers TempId] (TempIdHandler.))))))
40 |
41 | #?(:clj
42 | (defn writer
43 | "Create a transit writer.
44 |
45 | - `out`: An acceptable output for transit writers.
46 | - `opts`: (optional) options to pass to `cognitect.transit/writer` (such as data type handlers)."
47 | ([out]
48 | (writer out {}))
49 | ([out opts]
50 | (t/writer out :json
51 | (assoc-in opts [:handlers TempId] (TempIdHandler.))))))
52 |
53 | #?(:cljs
54 | (defn reader
55 | "Create a transit reader.
56 |
57 | - `opts`: (optional) options to pass to `cognitect.transit/reader` (such as data type handlers)."
58 | ([]
59 | (reader {}))
60 | ([opts]
61 | (t/reader :json
62 | (assoc-in opts
63 | [:handlers tempid/tag]
64 | (fn [id] (tempid/tempid id)))))))
65 |
66 | #?(:clj
67 | (defn reader
68 | "Create a transit reader.
69 |
70 | - `opts`: (optional) options to pass to `cognitect.transit/reader` (such as data type handlers)."
71 | ([in]
72 | (reader in {}))
73 | ([in opts]
74 | (t/reader in :json
75 | (assoc-in opts
76 | [:handlers tempid/tag]
77 | (reify
78 | ReadHandler
79 | (fromRep [_ id] (TempId. id))))))))
80 |
81 | (defn serializable?
82 | "Checks to see that the value in question can be serialized by the default fulcro writer by actually attempting to
83 | serialize it. This is *not* an efficient check."
84 | [v]
85 | #?(:clj (try
86 | (.write (writer (java.io.ByteArrayOutputStream.)) v)
87 | true
88 | (catch Exception e false))
89 | :cljs (try
90 | (.write (writer) v)
91 | true
92 | (catch :default e false))))
93 |
94 | (defn transit-clj->str
95 | "Use transit to encode clj data as a string. Useful for encoding initial app state from server-side rendering.
96 |
97 | - `data`: Arbitrary data
98 | - `opts`: (optional) Options to send when creating a `writer`."
99 | ([data] (transit-clj->str data {}))
100 | ([data opts]
101 | #?(:cljs (t/write (writer opts) data)
102 | :clj
103 | (with-open [out (java.io.ByteArrayOutputStream.)]
104 | (t/write (writer out opts) data)
105 | (.toString out "UTF-8")))))
106 |
107 | (defn transit-str->clj
108 | "Use transit to decode a string into a clj data structure. Useful for decoding initial app state when starting from a server-side rendering."
109 | ([str] (transit-str->clj str {}))
110 | ([str opts]
111 | #?(:cljs (t/read (reader opts) str)
112 | :clj (t/read (reader (java.io.ByteArrayInputStream. (.getBytes str "UTF-8")) opts)))))
113 |
114 |
--------------------------------------------------------------------------------
/src/main/com/fulcrologic/fulcro/algorithms/tempid.cljc:
--------------------------------------------------------------------------------
1 | (ns com.fulcrologic.fulcro.algorithms.tempid
2 | "Functions for making and consuming Fulcro temporary IDs."
3 | (:refer-clojure :exclude [uuid])
4 | (:require
5 | [taoensso.timbre :as log]
6 | [clojure.walk :refer [prewalk-replace]])
7 | #?(:clj (:import [java.io Writer])))
8 |
9 | (def tag "fulcro/tempid")
10 |
11 | ;; =============================================================================
12 | ;; ClojureScript
13 |
14 | #?(:cljs
15 | (deftype TempId [^:mutable id ^:mutable __hash]
16 | Object
17 | (toString [this]
18 | (pr-str this))
19 | IEquiv
20 | (-equiv [this other]
21 | (and (instance? TempId other)
22 | (= (. this -id) (. other -id))))
23 | IHash
24 | (-hash [this]
25 | (when (nil? __hash)
26 | (set! __hash (hash id)))
27 | __hash)
28 | IPrintWithWriter
29 | (-pr-writer [_ writer _]
30 | (write-all writer "#" tag "[\"" id "\"]"))))
31 |
32 | #?(:cljs
33 | (defn tempid
34 | "Create a new tempid."
35 | ([]
36 | (tempid (random-uuid)))
37 | ([id]
38 | (TempId. id nil))))
39 |
40 | ;; =============================================================================
41 | ;; Clojure
42 |
43 | #?(:clj
44 | (defrecord TempId [id]
45 | Object
46 | (toString [this]
47 | (pr-str this))))
48 |
49 | #?(:clj
50 | (defmethod print-method TempId [^TempId x ^Writer writer]
51 | (.write writer (str "#" tag "[\"" (.id x) "\"]"))))
52 |
53 | #?(:clj
54 | (defn tempid
55 | "Create a new tempid."
56 | ([]
57 | (tempid (java.util.UUID/randomUUID)))
58 | ([uuid]
59 | (TempId. uuid))))
60 |
61 | (defn tempid?
62 | "Returns true if the given `x` is a tempid."
63 | #?(:cljs {:tag boolean})
64 | [x]
65 | (instance? TempId x))
66 |
67 | (defn result->tempid->realid
68 | "Find and combine all of the tempid remappings from a standard fulcro transaction response."
69 | [tx-result]
70 | (let [get-tempids (fn [m] (or (get m :tempids)))]
71 | (->> (filter (comp symbol? first) tx-result)
72 | (map (comp get-tempids second))
73 | (reduce merge {}))))
74 |
75 | (defn resolve-tempids
76 | "Replaces all tempids in `data-structure` using the `tid->rid` map. This is just a deep
77 | walk that replaces every possible match of `tid` with `rid`.
78 |
79 | `tid->rid` must be a map, as this function optimizes away resolution by checking if
80 | the map is empty.
81 |
82 | Returns the data structure with everything replaced."
83 | [data-structure tid->rid]
84 | (if (empty? tid->rid)
85 | data-structure
86 | (prewalk-replace tid->rid data-structure)))
87 |
88 | (defn resolve-tempids!
89 | "Resolve all of the mutation tempid remappings in the `tx-result` against the given `app`.
90 |
91 | app - The fulcro app
92 | tx-result - The transaction result (the body map, not the internal tx node).
93 |
94 | This function rewrites all tempids in the app state and runtime transaction queues.
95 |
96 | NOTE: This function assumes that tempids are distinctly recognizable (e.g. are TempIds or
97 | guids). It is unsafe to use this function if you're using something else for temporary IDs
98 | as this function might rewrite things that are not IDs."
99 | [{:com.fulcrologic.fulcro.application/keys [state-atom runtime-atom]} tx-result]
100 | (let [tid->rid (result->tempid->realid tx-result)]
101 | (swap! state-atom resolve-tempids tid->rid)
102 | (swap! runtime-atom
103 | (fn [r]
104 | (-> r
105 | (update :com.fulcrologic.fulcro.transactions/submission-queue resolve-tempids tid->rid)
106 | (update :com.fulcrologic.fulcro.transactions/active-queue resolve-tempids tid->rid)
107 | (update :com.fulcrologic.fulcro.transactions/send-queues resolve-tempids tid->rid))))))
108 |
109 | (defn uuid
110 | "Generate a UUID. With no args returns a random UUID. with an arg (numeric)
111 | it generates a stable one based on that number (useful for testing). Works in cljc."
112 | #?(:clj ([] (java.util.UUID/randomUUID)))
113 | #?(:clj ([n]
114 | (java.util.UUID/fromString
115 | (format "ffffffff-ffff-ffff-ffff-%012d" n))))
116 | #?(:cljs ([] (random-uuid)))
117 | #?(:cljs ([& args] (cljs.core/uuid (apply str args)))))
118 |
119 |
--------------------------------------------------------------------------------
/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/do_not_use.cljc:
--------------------------------------------------------------------------------
1 | (ns com.fulcrologic.fulcro.algorithms.do-not-use
2 | "Some misc. utility functions. These are primarily meant for internal use, and are subject to
3 | relocation and removal in the future.
4 |
5 | You have been warned. Changes to this ns (or its complete removal)
6 | will not be considered breaking changes to the library, and no mention of said changes
7 | will even appear in the changelog."
8 | (:require
9 | [taoensso.timbre :as log]
10 | [edn-query-language.core :as eql]
11 | #?(:cljs [goog.object :as gobj])
12 | [clojure.spec.alpha :as s])
13 | #?(:clj
14 | (:import (clojure.lang Atom))))
15 |
16 | (defn atom? [a] (instance? Atom a))
17 |
18 | (defn join-entry [expr]
19 | (let [[k v] (if (seq? expr)
20 | (ffirst expr)
21 | (first expr))]
22 | [(if (list? k) (first k) k) v]))
23 |
24 | (defn join? [x]
25 | #?(:cljs {:tag boolean})
26 | (let [x (if (seq? x) (first x) x)]
27 | (map? x)))
28 |
29 | (defn recursion?
30 | #?(:cljs {:tag boolean})
31 | [x]
32 | (or #?(:clj (= '... x)
33 | :cljs (symbol-identical? '... x))
34 | (number? x)))
35 |
36 | (defn union?
37 | #?(:cljs {:tag boolean})
38 | [expr]
39 | (let [expr (cond-> expr (seq? expr) first)]
40 | (and (map? expr)
41 | (map? (-> expr first second)))))
42 |
43 | (defn join-key [expr]
44 | (cond
45 | (map? expr) (let [k (ffirst expr)]
46 | (if (list? k)
47 | (first k)
48 | (ffirst expr)))
49 | (seq? expr) (join-key (first expr))
50 | :else expr))
51 |
52 | (defn join-value [join]
53 | (second (join-entry join)))
54 |
55 | (defn mutation-join? [expr]
56 | (and (join? expr) (symbol? (join-key expr))))
57 |
58 | (defn now
59 | "Returns current time in ms."
60 | []
61 | #?(:clj (java.util.Date.)
62 | :cljs (js/Date.)))
63 |
64 | (defn deep-merge [& xs]
65 | "Merges nested maps without overwriting existing keys."
66 | (if (every? map? xs)
67 | (apply merge-with deep-merge xs)
68 | (last xs)))
69 |
70 | (defn conform! [spec x]
71 | (let [rt (s/conform spec x)]
72 | (when (s/invalid? rt)
73 | (throw (ex-info (s/explain-str spec x)
74 | (s/explain-data spec x))))
75 | rt))
76 |
77 | (defn destructured-keys
78 | "Calculates the keys that are being extracted in a legal map destructuring expression.
79 |
80 | - `m`: A map containing legal CLJ destructurings, like `{:keys [a] x :x ::keys [y]}`
81 |
82 | Returns a set of all keywords that are destructured in the map.
83 |
84 | Example:
85 |
86 | ```
87 | (destructured-keys {:a/keys [v] sym :other-key}) => #{:a/v :other-key}
88 | ```
89 | "
90 | [m]
91 | (let [regular-destructurings (reduce
92 | (fn [acc k]
93 | (if (and (keyword? k) (= "keys" (name k)))
94 | (let [simple-syms (get m k)
95 | included-ns (namespace k)
96 | source-keys (into #{}
97 | (map (fn [s]
98 | (cond
99 | included-ns (keyword included-ns (name s))
100 | (and (keyword? s) (namespace s)) s
101 | (namespace s) (keyword (namespace s) (name s))
102 | :else (keyword s))))
103 | simple-syms)]
104 | (into acc source-keys))
105 | acc))
106 | #{}
107 | (keys m))
108 | symbol-destructrings (reduce
109 | (fn [acc k]
110 | (if (symbol? k)
111 | (conj acc (get m k))
112 | acc))
113 | #{}
114 | (keys m))]
115 | (into regular-destructurings symbol-destructrings)))
116 |
--------------------------------------------------------------------------------
/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/test/com/fulcrologic/fulcro/algorithms/indexing_spec.cljc:
--------------------------------------------------------------------------------
1 | (ns com.fulcrologic.fulcro.algorithms.indexing-spec
2 | (:require
3 | [com.fulcrologic.fulcro.components :as comp :refer [defsc]]
4 | [com.fulcrologic.fulcro.application :as app]
5 | [com.fulcrologic.fulcro.algorithms.indexing :as idx]
6 | [fulcro-spec.core :refer [specification assertions behavior component when-mocking]]
7 | [edn-query-language.core :as eql]))
8 |
9 | (declare => =throws=>)
10 |
11 | (defsc UnionChildA [_ _]
12 | {:ident [:union-child/by-id :L]
13 | :query [:L]})
14 |
15 | (def ui-a (comp/factory UnionChildA))
16 |
17 | (defsc UnionChildB [_ _]
18 | {:query [:M]})
19 |
20 | (def ui-b (comp/factory UnionChildB))
21 |
22 | (defsc Union [_ _]
23 | {:query (fn [] {:u1 (comp/get-query ui-a)
24 | :u2 (comp/get-query ui-b)})})
25 |
26 | (def ui-union (comp/factory Union))
27 |
28 | (defsc Child [_ _]
29 | {:query [:x]})
30 |
31 | (def ui-child (comp/factory Child))
32 |
33 | (defsc Root [_ _]
34 | {:query [:a
35 | {:join (comp/get-query ui-child)}
36 | {:union (comp/get-query ui-union)}]})
37 |
38 | (def ui-root (comp/factory Root))
39 |
40 | (defsc UnionChildAP [_ _]
41 | {:query [`(:L {:child-params 1})]})
42 |
43 | (def ui-ap (comp/factory UnionChildAP))
44 |
45 | (defsc UnionP [_ _]
46 | {:query (fn [] {:u1 (comp/get-query ui-ap)
47 | :u2 (comp/get-query ui-b)})})
48 |
49 | (def ui-unionp (comp/factory UnionP))
50 |
51 | (defsc RootP [_ _]
52 | {:query [:a
53 | `({:join ~(comp/get-query ui-child)} {:join-params 2})
54 | `({:union ~(comp/get-query ui-unionp)} {:union-params 3})]})
55 |
56 | (defsc LinkChild [_ _]
57 | {:query [[:root/prop '_] {[:table 1] (comp/get-query Child)}]})
58 |
59 | (defsc RootLinks [_ _]
60 | {:query [:root/prop {:left (comp/get-query LinkChild)}]})
61 |
62 | (specification "index-query"
63 | (let [prop->classes (idx/index-query (comp/get-query Root))]
64 | (assertions
65 | "Properly indexes components with props, joins, and unions"
66 | (prop->classes :L) => #{::UnionChildA}
67 | (prop->classes :M) => #{::UnionChildB}
68 | (prop->classes :a) => #{::Root}
69 | (prop->classes :join) => #{::Root}
70 | (prop->classes :union) => #{::Root}))
71 | (let [prop->classes (idx/index-query (comp/get-query RootP))]
72 | (assertions
73 | "Properly indexes components that have parameterized queries"
74 | (prop->classes :L) => #{::UnionChildAP}
75 | (prop->classes :M) => #{::UnionChildB}
76 | (prop->classes :a) => #{::RootP}
77 | (prop->classes :join) => #{::RootP}
78 | (prop->classes :union) => #{::RootP}))
79 | (let [prop->classes (idx/index-query (comp/get-query RootLinks))]
80 | (assertions
81 | "Properly indexes components that have link queries and ident joins"
82 | (prop->classes :left) => #{::RootLinks}
83 | (prop->classes :root/prop) => #{::RootLinks ::LinkChild}
84 | (prop->classes [:table 1]) => #{::LinkChild})))
85 |
86 | (specification "link-query-props"
87 | (let [ast (eql/query->ast (comp/get-query RootLinks))
88 | linked-props (idx/link-query-props ast)]
89 | (assertions
90 | "is a set of the props that appear in link queries"
91 | linked-props => #{:root/prop})))
92 |
93 | (specification "top-level-keys"
94 | (let [ast (eql/query->ast (comp/get-query RootLinks))
95 | root-props (idx/top-level-keys ast)]
96 | (assertions
97 | "is a set of the props that appear in just the root of an ast"
98 | root-props => #{:root/prop :left})))
99 |
100 | (specification "index-component*"
101 | (let [runtime-state {::app/indexes {}}
102 | ra2 (#'idx/index-component* runtime-state :instance1 [:x 1] LinkChild)]
103 | (assertions
104 | "adds the component instance to the ident index"
105 | (-> ra2 ::app/indexes :ident->components (get [:x 1])) => #{:instance1}
106 | "adds the component instance to the class index"
107 | (-> ra2 ::app/indexes :class->components (get ::LinkChild)) => #{:instance1})))
108 |
109 | (specification "drop-component*"
110 | (let [runtime-state {::app/indexes {:ident->components {[:x 1] #{:instance1}}
111 | :class->components {::LinkChild #{:instance1}}}}
112 | ra2 (#'idx/drop-component* runtime-state :instance1 [:x 1] LinkChild)]
113 | (assertions
114 | "removes the component instance from the ident index"
115 | (-> ra2 ::app/indexes :ident->components (get [:x 1])) => #{}
116 | "removes the component instance from the class index"
117 | (-> ra2 ::app/indexes :class->components (get ::LinkChild)) => #{})))
118 |
--------------------------------------------------------------------------------
/src/test/com/fulcrologic/fulcro/dom_server_spec.clj:
--------------------------------------------------------------------------------
1 | (ns com.fulcrologic.fulcro.dom-server-spec
2 | (:require
3 | [fulcro-spec.core :refer [specification behavior assertions provided component when-mocking]]
4 | [com.fulcrologic.fulcro.components :as comp :refer [defsc]]
5 | [com.fulcrologic.fulcro.dom-server :as dom :refer [div p span render-to-str]]))
6 |
7 | (defsc Sample [this props] (dom/div "Hello"))
8 | (def ui-sample (comp/factory Sample))
9 |
10 | (specification "Server-side Rendering"
11 | (assertions
12 | "Simple tag rendering"
13 | (render-to-str (div {} "Hello"))
14 | => "Hello
"
15 | "strings adjacent to elements"
16 | (render-to-str (div {} "hello" (div)))
17 | => ""
18 | "Rendering with missing props"
19 | (render-to-str (div "Hello"))
20 | => "Hello
"
21 | "Rendering with kw props"
22 | (render-to-str (div :.a#1 "Hello"))
23 | => "Hello
"
24 | "Rendering with kw and props map"
25 | (render-to-str (div :.a#1 {:className "b"} "Hello"))
26 | => "Hello
"
27 | (render-to-str (div :.a#1 {:className "b" :classes ["x" :.c]} "Hello"))
28 | => "Hello
"
29 | "Nested rendering"
30 | (render-to-str (div :.a#1 {:className "b"}
31 | (p "P")
32 | (p :.x (span "PS2"))))
33 | => ""
34 | "Component child in DOM with props"
35 | (render-to-str (dom/div {:className "test"} (ui-sample {})))
36 | => ""
37 | "Component child in DOM with kw shortcut"
38 | (render-to-str (dom/div :.TEST (ui-sample {})))
39 | => ""
40 | "works with threading macro"
41 | (render-to-str (->>
42 | (span "PS2")
43 | (p :.x)
44 | (div :.a#1 {:className "b"})))
45 | => ""))
46 |
47 | (specification "DOM elements are usable as functions"
48 | (provided "The correct SSR function is called."
49 | (dom/element opts) => (assertions
50 | (:tag opts) => 'div)
51 |
52 | (apply div {} ["Hello"])))
53 |
54 | (specification "Fragments"
55 | (assertions
56 | "Allow multiple elements to be combined into a parent"
57 | (dom/render-to-str (dom/div (comp/fragment {:key 1} (dom/p "a") (dom/p "b"))))
58 | =>
59 | ""
60 |
61 | "Props are optional"
62 | (dom/render-to-str (dom/div (comp/fragment (dom/p "a") (dom/p "b"))))
63 | =>
64 | ""))
65 |
66 | (defsc Child [this props]
67 | (apply dom/div {}
68 | (comp/children this)))
69 |
70 | (def ui-child (comp/factory Child))
71 |
72 | (defsc Root [this props]
73 | (ui-child {}
74 | (ui-sample {})
75 | (ui-sample {})))
76 |
77 | (def ui-root (comp/factory Root))
78 |
79 | (specification "Children support"
80 | (assertions
81 | "Allow multiple children to be passed to a component"
82 | (dom/render-to-str (ui-root {}))
83 | =>
84 | ""))
85 |
86 | (defsc LCC [this props]
87 | {:initLocalState (fn [this] {:x 2})}
88 | (let [x (comp/get-state this :x)]
89 | (dom/div x)))
90 |
91 | (def ui-lcc (comp/factory LCC))
92 |
93 | (specification "Component-local state support"
94 | (assertions
95 | "Renders the correct result when local state is used"
96 | (dom/render-to-str (ui-lcc {}))
97 | =>
98 | "2
"))
99 |
--------------------------------------------------------------------------------
/src/main/com/fulcrologic/fulcro/algorithms/normalize.cljc:
--------------------------------------------------------------------------------
1 | (ns com.fulcrologic.fulcro.algorithms.normalize
2 | "Functions for dealing with normalizing Fulcro databases. In particular `tree->db`."
3 | (:require
4 | [com.fulcrologic.fulcro.algorithms.do-not-use :as util]
5 | [edn-query-language.core :as eql]
6 | [taoensso.timbre :as log]
7 | [com.fulcrologic.fulcro.components :refer [has-ident? ident get-ident get-query]]))
8 |
9 | (defn- normalize* [query data refs union-seen transform]
10 | (let [data (if (and transform (not (vector? data)))
11 | (transform query data)
12 | data)]
13 | (cond
14 | (= '[*] query) data
15 |
16 | ;; union case
17 | (map? query)
18 | (let [class (-> query meta :component)
19 | ident (get-ident class data)]
20 | (if-not (nil? ident)
21 | (vary-meta (normalize* (get query (first ident)) data refs union-seen transform)
22 | assoc ::tag (first ident)) ; FIXME: What is tag for?
23 | (throw (ex-info "Union components must have an ident" {}))))
24 |
25 | (vector? data) data ;; already normalized
26 |
27 | :else
28 | (loop [q (seq query) ret data]
29 | (if-not (nil? q)
30 | (let [expr (first q)]
31 | (if (util/join? expr)
32 | (let [[k sel] (util/join-entry expr)
33 | recursive? (util/recursion? sel)
34 | union-entry (if (util/union? expr) sel union-seen)
35 | sel (if recursive?
36 | (if-not (nil? union-seen)
37 | union-seen
38 | query)
39 | sel)
40 | class (-> sel meta :component)
41 | v (get data k)]
42 | (cond
43 | ;; graph loop: db->tree leaves ident in place
44 | (and recursive? (eql/ident? v)) (recur (next q) ret)
45 | ;; normalize one
46 | (map? v)
47 | (let [x (normalize* sel v refs union-entry transform)]
48 | (if-not (or (nil? class) (not (has-ident? class)))
49 | (let [i (get-ident class x)]
50 | (swap! refs update-in [(first i) (second i)] merge x)
51 | (recur (next q) (assoc ret k i)))
52 | (recur (next q) (assoc ret k x))))
53 |
54 | ;; normalize many
55 | (and (vector? v) (not (eql/ident? v)) (not (eql/ident? (first v))))
56 | (let [xs (into [] (map #(normalize* sel % refs union-entry transform)) v)]
57 | (if-not (or (nil? class) (not (has-ident? class)))
58 | (let [is (into [] (map #(get-ident class %)) xs)]
59 | (if (vector? sel)
60 | (when-not (empty? is)
61 | (swap! refs
62 | (fn [refs]
63 | (reduce (fn [m [i x]]
64 | (update-in m i merge x))
65 | refs (zipmap is xs)))))
66 | ;; union case
67 | (swap! refs
68 | (fn [refs']
69 | (reduce
70 | (fn [ret [i x]]
71 | (update-in ret i merge x))
72 | refs' (map vector is xs)))))
73 | (recur (next q) (assoc ret k is)))
74 | (recur (next q) (assoc ret k xs))))
75 |
76 | ;; missing key
77 | (nil? v)
78 | (recur (next q) ret)
79 |
80 | ;; can't handle
81 | :else (recur (next q) (assoc ret k v))))
82 | (let [k (if (seq? expr) (first expr) expr)
83 | v (get data k)]
84 | (if (nil? v)
85 | (recur (next q) ret)
86 | (recur (next q) (assoc ret k v))))))
87 | ret)))))
88 |
89 | (defn tree->db
90 | "Given a component class or instance and a tree of data, use the component's
91 | query to transform the tree into the default database format. All nodes that
92 | can be mapped via Ident implementations wil be replaced with ident links. The
93 | original node data will be moved into tables indexed by ident. If merge-idents
94 | option is true, will return these tables in the result instead of as metadata."
95 | ([x data]
96 | (tree->db x data false))
97 | ([x data #?(:clj merge-idents :cljs ^boolean merge-idents)]
98 | (tree->db x data merge-idents nil))
99 | ([x data #?(:clj merge-idents :cljs ^boolean merge-idents) transform]
100 | (let [refs (atom {})
101 | x (if (vector? x) x (get-query x data))
102 | ret (normalize* x data refs nil transform)]
103 | (if merge-idents
104 | (let [refs' @refs] (merge ret refs'))
105 | (with-meta ret @refs)))))
106 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 |
6 | com.fulcrologic
7 | fulcro
8 | jar
9 | 3.0.0-beta-1
10 |
11 | fulcro
12 | A library for building full-stack SPA webapps in Clojure and Clojurescript
13 | https://github.com/fulcrologic/fulcro
14 |
15 |
16 |
17 | MIT
18 | https://opensource.org/licenses/MIT
19 |
20 |
21 |
22 |
23 | https://github.com/fulcrologic/fulcro3
24 | scm:git:git://github.com/fulcrologic/fulcro3.git
25 | scm:git:ssh://git@github.com/fulcrologic/fulcro3.git
26 | 9e75c6dd6b2346aefb12ba5c40affa051543e9f3
27 |
28 |
29 |
30 |
31 | clojars
32 | Clojars repository
33 | https://clojars.org/repo
34 |
35 |
36 |
37 |
38 | src/main
39 |
40 |
41 |
42 | src/main
43 |
44 |
45 |
46 |
47 |
48 | org.apache.maven.plugins
49 | maven-gpg-plugin
50 |
51 |
52 | sign-artifacts
53 | verify
54 |
55 | sign
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | clojars
66 | https://repo.clojars.org/
67 |
68 |
69 |
70 |
71 |
72 | org.clojure
73 | clojure
74 | 1.10.0
75 |
76 |
77 | com.wsscode
78 | pathom
79 | 2.2.14
80 |
81 |
82 | edn-query-language
83 | eql
84 | 0.0.7
85 |
86 |
87 | com.taoensso
88 | encore
89 | 2.94.0
90 |
91 |
92 | cljsjs
93 | react-dom
94 | 16.8.6-0
95 |
96 |
97 | com.taoensso
98 | tufte
99 | 2.0.1
100 |
101 |
102 | com.cognitect
103 | transit-cljs
104 | 0.8.256
105 |
106 |
107 | org.clojure
108 | clojurescript
109 | 1.10.520
110 |
111 |
112 | gnl
113 | ghostwheel
114 | 0.3.9
115 |
116 |
117 | cljsjs
118 | react
119 | 16.8.6-0
120 |
121 |
122 | com.taoensso
123 | timbre
124 | 4.10.0
125 |
126 |
127 | com.cognitect
128 | transit-clj
129 | 0.8.313
130 |
131 |
132 | cljsjs
133 | react-dom-server
134 | 16.8.6-0
135 |
136 |
137 | org.clojure
138 | core.async
139 | 0.4.474
140 |
141 |
142 |
143 |
--------------------------------------------------------------------------------
/src/main/com/fulcrologic/fulcro/server/config.clj:
--------------------------------------------------------------------------------
1 | (ns com.fulcrologic.fulcro.server.config
2 | "Utilities for managing server configuration via EDN files. These functions expect a config/defaults.edn to exist
3 | on the classpath as a definition for server configuration default values. When you call `load-config!` it will
4 | deep merge the file you supply with the base defaults to return the 'complete' configuration. When loading
5 | configurations a relative path is evaluated against CLASSPATH and an absolute path against the real filesystem.
6 |
7 | The values in the EDN files can be :env/VAR to pull a string from an env variable, and :env.edn/VAR to do a `read-string`
8 | against the value of an environment variable."
9 | (:require
10 | [clojure.java.io :as io]
11 | [clojure.edn :as edn]
12 | [clojure.walk :as walk]
13 | [ghostwheel.core :refer [>defn =>]]
14 | [taoensso.timbre :as log]
15 | [com.fulcrologic.fulcro.algorithms.do-not-use :as util]
16 | [clojure.spec.alpha :as s]))
17 |
18 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
19 | ;; CONFIG
20 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
21 |
22 | (defn get-system-prop [prop-name]
23 | (System/getProperty prop-name))
24 |
25 | (defn- load-edn!
26 | "If given a relative path, looks on classpath (via class loader) for the file, reads the content as EDN, and returns it.
27 | If the path is an absolute path, it reads it as EDN and returns that.
28 | If the resource is not found, returns nil.
29 |
30 | This function returns the EDN file without further interpretation (no merging or env evaluation). Normally you want
31 | to use `load-config!` instead."
32 | [^String file-path]
33 | (let [?edn-file (io/file file-path)]
34 | (if-let [edn-file (and (.isAbsolute ?edn-file)
35 | (.exists ?edn-file)
36 | (io/file file-path))]
37 | (-> edn-file slurp edn/read-string)
38 | (some-> file-path io/resource .openStream slurp edn/read-string))))
39 |
40 | (defn- load-edn-file!
41 | "Calls load-edn on `file-path`,
42 | and throws an ex-info if that failed."
43 | [file-path]
44 | (log/info "Reading configuration file at " file-path)
45 | (if-let [edn (some-> file-path load-edn!)]
46 | edn
47 | (do
48 | (log/error "Unable to read configuration file " file-path)
49 | (throw (ex-info (str "Invalid config file at '" file-path "'")
50 | {:file-path file-path})))))
51 |
52 | (defn- resolve-symbol [sym]
53 | {:pre [(namespace sym)]
54 | :post [(not (nil? %))]}
55 | (or (resolve sym)
56 | (do (-> sym namespace symbol require)
57 | (resolve sym))))
58 |
59 | (defn- get-system-env [var-name]
60 | (System/getenv var-name))
61 |
62 | (defn load-config!
63 | "Load a configuration file via the given options.
64 |
65 | options is a map with keys:
66 |
67 | * `:config-path` : The path to the file to load (in addition to the addl behavior described below).
68 | * `:defaults-path` : (optional) A relative or absolute path to the default options that should be the basis of configuration.
69 | Defaults to `config/defaults.edn`. When relative, will come from resources. When absolute, will come from disk.
70 |
71 | Reads the defaults, then deep merges the EDN content
72 | of an additional config file you specify into that and evaluates environment variable expansions.
73 |
74 | You may use a Java system property to specify (override) the config file used:
75 |
76 | ```
77 | java -Dconfig=/usr/local/etc/app.edn ...
78 | ```
79 |
80 | If no such property is used then config-path MUST be supplied (or this will throw an exception).
81 |
82 | Values in the EDN of the form :env/VAR mean to use the raw string value of an environment variable, and
83 | :env.edn/VAR mean to use the `read-string` value of the environment variable as that value.
84 |
85 | So the classpath resource config/defaults.edn might contain:
86 |
87 | ```
88 | {:port 3000
89 | :service :A}
90 | ```
91 |
92 | and `/usr/local/etc/app.edn` might contain:
93 |
94 | ```
95 | {:port :env.edn/PORT}
96 | ```
97 |
98 | and a call to `(load-config! {:config-path \"/usr/local/etc/app.edn\"})` on a system with env variable `PORT=\"8080\"` would return:
99 |
100 | ```
101 | {:port 8080 ;; as an integer, not a string
102 | :service :A}
103 | ```
104 |
105 | If your EDN file includes a symbol (which must be namespaced) then it will try to require and resolve
106 | it dynamically as the configuration loads.
107 | "
108 | ([] (load-config! {}))
109 | ([{:keys [config-path defaults-path]}]
110 | (let [defaults-path (if (seq defaults-path)
111 | defaults-path
112 | "config/defaults.edn")
113 | defaults (load-edn-file! defaults-path)
114 | config (load-edn-file! (or (get-system-prop "config") config-path))]
115 | (->> (util/deep-merge defaults config)
116 | (walk/prewalk #(cond-> % (symbol? %) resolve-symbol
117 | (and (keyword? %) (namespace %)
118 | (re-find #"^env.*" (namespace %)))
119 | (-> name get-system-env
120 | (cond-> (= "env.edn" (namespace %))
121 | (edn/read-string)))))))))
122 |
123 | (def ^:deprecated load-config
124 | "Use load-config!"
125 | load-config!)
126 |
127 | (def ^:deprecated open-config-file
128 | "Not meant for public consumption"
129 | load-edn-file!)
130 |
131 | (def ^:deprecated load-edn
132 | "Not meant for public consumption"
133 | load-edn!)
134 |
--------------------------------------------------------------------------------
/src/todomvc/fulcro_todomvc/api.cljs:
--------------------------------------------------------------------------------
1 | (ns fulcro-todomvc.api
2 | (:require
3 | [com.fulcrologic.fulcro.mutations :as m :refer [defmutation]]
4 | [com.fulcrologic.fulcro.algorithms.data-targeting :as dt]
5 | [edn-query-language.core :as eql]
6 | [taoensso.timbre :as log]
7 | [com.fulcrologic.fulcro.application :as app]
8 | [com.fulcrologic.fulcro.components :as comp]
9 | [com.fulcrologic.fulcro.algorithms.merge :as merge]
10 | [clojure.string :as str]))
11 |
12 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
13 | ;; Client-side API
14 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
15 |
16 | (defn add-item-to-list*
17 | "Add an item's ident onto the end of the given list."
18 | [state-map list-id item-id]
19 | (update-in state-map [:list/id list-id :list/items] (fnil conj []) [:item/id item-id]))
20 |
21 | (defn create-item*
22 | "Create a new todo item and insert it into the todo item table."
23 | [state-map id text]
24 | (assoc-in state-map [:item/id id] {:item/id id :item/label text}))
25 |
26 | (defn set-item-checked*
27 | [state-map id checked?]
28 | (assoc-in state-map [:item/id id :item/complete] checked?))
29 |
30 | (defn clear-list-input-field*
31 | "Clear the main input field of the todo list"
32 | [state-map id]
33 | (assoc-in state-map [:list/id id :ui/new-item-text] ""))
34 |
35 | (defmutation todo-new-item [{:keys [list-id id text]}]
36 | (action [{:keys [state ast]}]
37 | (swap! state #(-> %
38 | (create-item* id text)
39 | (add-item-to-list* list-id id)
40 | (clear-list-input-field* list-id))))
41 | (remote [_] true))
42 |
43 | (defmutation todo-check
44 | "Check the given item, by id."
45 | [{:keys [id]}]
46 | (action [{:keys [state]}]
47 | (swap! state set-item-checked* id true))
48 | (remote [_] true))
49 |
50 | (defmutation todo-uncheck
51 | "Uncheck the given item, by id."
52 | [{:keys [id]}]
53 | (action [{:keys [state]}]
54 | (swap! state set-item-checked* id false))
55 | (remote [_] true))
56 |
57 | (defn set-item-label*
58 | "Set the given item's label"
59 | [state-map id text]
60 | (assoc-in state-map [:item/id id :item/label] text))
61 |
62 | (defmutation commit-label-change
63 | "Mutation: Commit the given text as the new label for the item with id."
64 | [{:keys [id text]}]
65 | (action [{:keys [state]}]
66 | (swap! state set-item-label* id text))
67 | (remote [_] true))
68 |
69 | (defn remove-from-idents
70 | "Given a vector of idents and an id, return a vector of idents that have none that use that ID for their second (id) element."
71 | [vec-of-idents id]
72 | (vec (filter (fn [ident] (not= id (second ident))) vec-of-idents)))
73 |
74 | (defmutation todo-delete-item [{:keys [list-id id]}]
75 | (action [{:keys [state]}]
76 | (swap! state #(-> %
77 | (update-in [:list/id list-id :list/items] remove-from-idents id)
78 | (update :item/id dissoc id))))
79 | (error-action [{:keys [result] :as env}]
80 | (log/info "Delete cancelled!!!" result))
81 | (remote [_] true))
82 |
83 | (defn on-all-items-in-list
84 | "Run the xform on all of the todo items in the list with list-id. The xform will be called with the state map and the
85 | todo's id and must return a new state map with that todo updated. The args will be applied to the xform as additional
86 | arguments"
87 | [state-map list-id xform & args]
88 | (let [item-idents (get-in state-map [:list/id list-id :list/items])]
89 | (reduce (fn [s idt]
90 | (let [id (second idt)]
91 | (apply xform s id args))) state-map item-idents)))
92 |
93 | (defmutation todo-check-all [{:keys [list-id]}]
94 | (action [{:keys [state]}]
95 | (swap! state on-all-items-in-list list-id set-item-checked* true))
96 | (remote [_] true))
97 |
98 | (defmutation todo-uncheck-all [{:keys [list-id]}]
99 | (action [{:keys [state]}]
100 | (swap! state on-all-items-in-list list-id set-item-checked* false))
101 | (remote [_] true))
102 |
103 | (defmutation todo-clear-complete [{:keys [list-id]}]
104 | (action [{:keys [state]}]
105 | (let [is-complete? (fn [item-ident] (get-in @state (conj item-ident :item/complete)))]
106 | (swap! state update-in [:list/id list-id :list/items]
107 | (fn [todos] (vec (remove (fn [ident] (is-complete? ident)) todos))))))
108 | (remote [_] true))
109 |
110 | (defn current-list-id [state] (get-in state [:application :root :todos 1]))
111 |
112 | (defmutation set-desired-filter
113 | "Check to see if there was a desired filter. If so, put it on the now-active list and remove the desire. This is
114 | necessary because the HTML5 routing event comes to us on app load before we can load the list."
115 | [ignored]
116 | (action [{:keys [state]}]
117 | (let [list-id (current-list-id @state)
118 | desired-filter (get @state :root/desired-filter)]
119 | (when (and list-id desired-filter)
120 | (swap! state assoc-in [:list/id list-id :list/filter] desired-filter)
121 | (swap! state dissoc :root/desired-filter)))))
122 |
123 | (defmutation todo-filter
124 | "Change the filter on the active list (the one pointed to by top-level :todos). If there isn't one, stash
125 | it in :root/desired-filter."
126 | [{:keys [filter]}]
127 | (action [{:keys [state]}]
128 | (let [list-id (current-list-id @state)]
129 | (if list-id
130 | (swap! state assoc-in [:list/id list-id :list/filter] filter)
131 | (swap! state assoc :root/desired-filter filter)))))
132 |
133 |
--------------------------------------------------------------------------------
/src/main/com/fulcrologic/fulcro/dom_common.cljc:
--------------------------------------------------------------------------------
1 | (ns com.fulcrologic.fulcro.dom-common
2 | (:refer-clojure :exclude [map meta time mask select use set symbol filter])
3 | (:require
4 | [clojure.string :as str]
5 | #?@(:cljs ([goog.object :as gobj]))))
6 |
7 | (defn- remove-separators [s]
8 | (when s
9 | (str/replace s #"^[.#]" "")))
10 |
11 | (defn- get-tokens [k]
12 | (re-seq #"[#.]?[^#.]+" (name k)))
13 |
14 | (defn- parse
15 | "Parse CSS shorthand keyword and return map of id/classes.
16 |
17 | (parse :.klass3#some-id.klass1.klass2)
18 | => {:id \"some-id\"
19 | :classes [\"klass3\" \"klass1\" \"klass2\"]}"
20 | [k]
21 | (if k
22 | (let [tokens (get-tokens k)
23 | id (->> tokens (clojure.core/filter #(re-matches #"^#.*" %)) first)
24 | classes (->> tokens (clojure.core/filter #(re-matches #"^\..*" %)))
25 | sanitized-id (remove-separators id)]
26 | (when-not (re-matches #"^(\.[^.#]+|#[^.#]+)+$" (name k))
27 | (throw (ex-info "Invalid style keyword. It contains something other than classnames and IDs." {:item k})))
28 | (cond-> {:classes (into []
29 | (keep remove-separators classes))}
30 | sanitized-id (assoc :id sanitized-id)))
31 | {}))
32 |
33 | (defn- combined-classes
34 | "Takes a sequence of classname strings and a string with existing classes. Returns a string of these properly joined.
35 |
36 | classes-str can be nil or and empty string, and classes-seq can be nil or empty."
37 | [classes-seq classes-str]
38 | (str/join " " (if (seq classes-str) (conj classes-seq classes-str) classes-seq)))
39 |
40 | (defn add-kwprops-to-props
41 | "Combine a hiccup-style keyword with props that are either a JS or CLJS map."
42 | [props kw]
43 | (let [{:keys [classes id] :or {classes []}} (parse kw)]
44 | (if #?(:clj false :cljs (or (nil? props) (object? props)))
45 | #?(:clj props
46 | :cljs (let [props (gobj/clone props)
47 | existing-classes (gobj/get props "className")]
48 | (when (seq classes) (gobj/set props "className" (combined-classes classes existing-classes)))
49 | (when id (gobj/set props "id" id))
50 | props))
51 | (let [existing-classes (:className props)]
52 | (cond-> (or props {})
53 | (seq classes) (assoc :className (combined-classes classes existing-classes))
54 | id (assoc :id id))))))
55 |
56 | (def tags '#{a abbr address altGlyph altGlyphDef altGlyphItem animate animateColor animateMotion animateTransform area
57 | article aside audio b base bdi bdo big blockquote body br button canvas caption circle cite clipPath code
58 | col colgroup color-profile cursor data datalist dd defs del desc details dfn dialog discard div dl dt
59 | ellipse em embed feBlend feColorMatrix feComponentTransfer feComposite feConvolveMatrix feDiffuseLighting
60 | feDisplacementMap feDistantLight feDropShadow feFlood feFuncA feFuncB feFuncG feFuncR feGaussianBlur
61 | feImage feMerge feMergeNode feMorphology feOffset fePointLight feSpecularLighting feSpotLight feTile feTurbulence
62 | fieldset figcaption figure filter font font-face font-face-format font-face-name font-face-src font-face-uri
63 | footer foreignObject form g glyph glyphRef h1 h2 h3 h4 h5 h6 hatch hatchpath head header hkern hr html
64 | i iframe image img input ins kbd keygen label legend li line linearGradient link main map mark marker mask
65 | menu menuitem mesh meshgradient meshpatch meshrow meta metadata meter missing-glyph
66 | mpath nav noscript object ol optgroup option output p param path pattern picture polygon polyline pre progress q radialGradient
67 | rect rp rt ruby s samp script section select set small solidcolor source span stop strong style sub summary
68 | sup svg switch symbol table tbody td text textPath textarea tfoot th thead time title tr track tref tspan
69 | u ul unknown use var video view vkern wbr})
70 |
71 | (defn gen-docstring
72 | "Helper function for generating the docstrings for generated dom functions and
73 | macros."
74 | [tag client-side?]
75 | (str "Returns a " (if client-side? "React" "server side")
76 | " DOM element. Can be invoked in several ways\n\n"
77 |
78 | "These two are made equivalent at compile time\n"
79 | "(" tag " \"hello\")\n"
80 | (str "(" tag " nil \"hello\")\n")
81 | "\n"
82 |
83 | "These two are made equivalent at compile time\n"
84 | "(" tag " {:onClick f} \"hello\")\n"
85 | "(" tag " #js {:onClick f} \"hello\")\n"
86 | "\n"
87 |
88 | "There is also a shorthand for CSS id and class names\n"
89 | "(" tag " :#the-id.klass.other-klass \"hello\")\n"
90 | "(" tag " :#the-id.klass.other-klass {:onClick f} \"hello\")"))
91 |
92 | (defn classes->str
93 | [classes]
94 | (str/join " "
95 | (into []
96 | (comp
97 | (mapcat (fn [entry]
98 | (cond
99 | (keyword? entry) (:classes (parse entry))
100 | (string? entry) [entry])))
101 | (clojure.core/filter string?))
102 | classes)))
103 |
104 | (defn interpret-classes
105 | "Interprets the :classes prop, reducing any non-nil elements into :className. returns the new props with updated
106 | :className and no :classes"
107 | [props]
108 | (if (and (map? props) (contains? props :classes))
109 | (let [new-class-strings (classes->str (:classes props))
110 | strcls (or (:className props) "")
111 | final-classes (str strcls " " new-class-strings)]
112 | (-> props
113 | (assoc :className final-classes)
114 | (dissoc :classes)))
115 | props))
116 |
--------------------------------------------------------------------------------
/src/todomvc/fulcro_todomvc/server.clj:
--------------------------------------------------------------------------------
1 | (ns fulcro_todomvc.server
2 | (:require
3 | [clojure.core.async :as async]
4 | [com.fulcrologic.fulcro.algorithms.do-not-use :as util]
5 | [com.fulcrologic.fulcro.server.api-middleware :as fmw :refer [not-found-handler wrap-api]]
6 | [com.wsscode.pathom.connect :as pc]
7 | [com.wsscode.pathom.core :as p]
8 | [immutant.web :as web]
9 | [ring.middleware.content-type :refer [wrap-content-type]]
10 | [ring.middleware.not-modified :refer [wrap-not-modified]]
11 | [ring.middleware.resource :refer [wrap-resource]]
12 | [ring.util.response :refer [response file-response resource-response]]
13 | [taoensso.timbre :as log]
14 | [clojure.tools.namespace.repl :as tools-ns]
15 | [com.fulcrologic.fulcro.algorithms.tempid :as tempid]))
16 |
17 | (def item-db (atom {1 {:item/id 1
18 | :item/label "Item 1"
19 | :item/complete false}
20 | 2 {:item/id 2
21 | :item/label "Item 2"
22 | :item/complete false}
23 | 3 {:item/id 3
24 | :item/label "Item 3"
25 | :item/complete false}}))
26 |
27 | (pc/defmutation todo-new-item [env {:keys [id list-id text]}]
28 | {::pc/sym `fulcro-todomvc.api/todo-new-item
29 | ::pc/params [:list-id :id :text]
30 | ::pc/output [:item/id]}
31 | (log/info "New item on server")
32 | (let [new-id (tempid/uuid)]
33 | (swap! item-db assoc new-id {:item/id new-id :item/label text :item/complete false})
34 | {:tempids {id new-id}
35 | :item/id new-id}))
36 |
37 | (pc/defmutation todo-check [env {:keys [id list-id]}]
38 | {::pc/sym `fulcro-todomvc.api/todo-check
39 | ::pc/params [:list-id :id]
40 | ::pc/output []}
41 | (log/info "Checked item" id)
42 | (swap! item-db assoc-in [id :item/complete] true)
43 | {})
44 |
45 | (pc/defmutation todo-uncheck [env {:keys [id list-id]}]
46 | {::pc/sym `fulcro-todomvc.api/todo-uncheck
47 | ::pc/params [:list-id :id]
48 | ::pc/output []}
49 | (log/info "Unchecked item" id)
50 | (swap! item-db assoc-in [id :item/complete] false)
51 | {})
52 |
53 | (pc/defmutation commit-label-change [env {:keys [id list-id text]}]
54 | {::pc/sym `fulcro-todomvc.api/commit-label-change
55 | ::pc/params [:list-id :id :text]
56 | ::pc/output []}
57 | (log/info "Set item label text of" id "to" text)
58 | (swap! item-db assoc-in [id :item/label] text)
59 | {})
60 |
61 | (pc/defmutation todo-delete-item [env {:keys [id list-id]}]
62 | {::pc/sym `fulcro-todomvc.api/todo-delete-item
63 | ::pc/params [:list-id :id]
64 | ::pc/output []}
65 | (log/info "Deleted item" id)
66 | (swap! item-db dissoc id)
67 | {})
68 |
69 | (defn- to-all-todos [db f]
70 | (into {}
71 | (map (fn [[id todo]]
72 | [id (f todo)]))
73 | db))
74 |
75 | (pc/defmutation todo-check-all [env {:keys [list-id]}]
76 | {::pc/sym `fulcro-todomvc.api/todo-check-all
77 | ::pc/params [:list-id]
78 | ::pc/output []}
79 | (log/info "Checked all items")
80 | (swap! item-db to-all-todos #(assoc % :item/complete true))
81 | {})
82 |
83 | (pc/defmutation todo-uncheck-all [env {:keys [list-id]}]
84 | {::pc/sym `fulcro-todomvc.api/todo-uncheck-all
85 | ::pc/params [:list-id]
86 | ::pc/output []}
87 | (log/info "Unchecked all items")
88 | (swap! item-db to-all-todos #(assoc % :item/complete false))
89 | {})
90 |
91 | (pc/defmutation todo-clear-complete [env {:keys [list-id]}]
92 | {::pc/sym `fulcro-todomvc.api/todo-clear-complete
93 | ::pc/params [:list-id]
94 | ::pc/output []}
95 | (log/info "Cleared completed items")
96 | (swap! item-db (fn [db] (into {} (remove #(-> % val :item/complete)) db)))
97 | {})
98 |
99 | ;; How to go from :person/id to that person's details
100 | (pc/defresolver list-resolver [env params]
101 | {::pc/input #{:list/id}
102 | ::pc/output [:list/title {:list/items [:item/id]}]}
103 | ;; normally you'd pull the person from the db, and satisfy the listed
104 | ;; outputs. For demo, we just always return the same person details.
105 | {:list/title "The List"
106 | :list/items (into []
107 | (sort-by :item/label (vals @item-db)))})
108 |
109 | ;; how to go from :address/id to address details.
110 | (pc/defresolver item-resolver [env {:keys [item/id] :as params}]
111 | {::pc/input #{:item/id}
112 | ::pc/output [:item/complete :item/label]}
113 | (get @item-db id))
114 |
115 | ;; define a list with our resolvers
116 | (def my-resolvers [list-resolver item-resolver
117 | todo-new-item commit-label-change todo-delete-item
118 | todo-check todo-uncheck
119 | todo-check-all todo-uncheck-all
120 | todo-clear-complete])
121 |
122 | ;; setup for a given connect system
123 | (def parser
124 | (p/parallel-parser
125 | {::p/env {::p/reader [p/map-reader
126 | pc/parallel-reader
127 | pc/open-ident-reader]
128 | ::pc/mutation-join-globals [:tempids]}
129 | ::p/mutate pc/mutate-async
130 | ::p/plugins [(pc/connect-plugin {::pc/register my-resolvers})
131 | (p/post-process-parser-plugin p/elide-not-found)
132 | p/error-handler-plugin]}))
133 |
134 | (def middleware (-> not-found-handler
135 | (wrap-api {:uri "/api"
136 | :parser (fn [query] (async/defn]]
8 | [clojure.set :as set]
9 | [edn-query-language.core :as eql]
10 | [taoensso.encore :as encore]
11 | [taoensso.timbre :as log]))
12 |
13 | (defn- index-query*
14 | [prop->classes {parent-component :component
15 | parent-children :children
16 | :as ast}]
17 | (let [parent-key (comp/class->registry-key parent-component)
18 | parent-children (seq parent-children)
19 | update-index (fn [idx k c] (update idx k (fnil conj #{}) c))]
20 | (if parent-children
21 | (reduce
22 | (fn [idx {:keys [key dispatch-key children] :as child-ast}]
23 | (cond-> idx
24 | (and (vector? key) (= '_ (second key))) (update-index dispatch-key parent-key)
25 | (and (vector? key) (not= '_ (second key))) (update-index key parent-key)
26 | (keyword? key) (update-index key parent-key)
27 | (seq children) (index-query* child-ast)))
28 | prop->classes
29 | parent-children)
30 | prop->classes)))
31 |
32 | (defn index-query
33 | "Create an index of the given component-annotated query. Returns a map from query keyword to the component
34 | class(es) that query for that keyword."
35 | [query]
36 | (let [ast (eql/query->ast query)]
37 | (index-query* {} ast)))
38 |
39 | (defn top-level-keys
40 | "Return a set of keywords that are in the top-level of the given AST"
41 | [ast]
42 | (let [{:keys [children]} ast]
43 | (into #{} (comp (map :key) (filter keyword?)) children)))
44 |
45 | (defn link-query-props
46 | "Returns a set of all of the keys that appear in link refs `[:k '_]` in the entire ast."
47 | [{:keys [key children] :as ast}]
48 | (cond
49 | (fdn/link-ref? key) (apply set/union #{(first key)} (map link-query-props children))
50 | (seq children) (apply set/union (map link-query-props children))
51 | :otherwise #{}))
52 |
53 | (defn index-root!
54 | "Index the root query (see index-query) and side-effect the result (`prop->classes`) into the given app.
55 | This function assumes the `root-class` has already been supplied to the app (i.e. is has been mounted)."
56 | [app]
57 | (log/debug "(Re)indexing application query for prop->classes")
58 | (let [{:com.fulcrologic.fulcro.application/keys [state-atom runtime-atom]} app
59 | {:com.fulcrologic.fulcro.application/keys [root-class]} @runtime-atom
60 | state-map @state-atom
61 | root-query (comp/get-query root-class state-map)
62 | ast (eql/query->ast root-query)
63 | prop->classes (index-query root-query)
64 | idents-in-joins (into #{} (filter eql/ident?) (keys prop->classes))
65 | root-props (top-level-keys ast)
66 | linked-props (link-query-props ast)]
67 | (swap! runtime-atom (fn [s]
68 | (-> s
69 | (assoc-in [:com.fulcrologic.fulcro.application/indexes :root-props] root-props)
70 | (assoc-in [:com.fulcrologic.fulcro.application/indexes :linked-props] linked-props)
71 | (assoc-in [:com.fulcrologic.fulcro.application/indexes :idents-in-joins] idents-in-joins)
72 | (assoc-in [:com.fulcrologic.fulcro.application/indexes :prop->classes] prop->classes))))))
73 |
74 | (defn- index-component* [runtime-state instance ident cls]
75 | (let [k (comp/class->registry-key cls)]
76 | (cond-> runtime-state
77 | k (update-in
78 | [:com.fulcrologic.fulcro.application/indexes :class->components k]
79 | (fnil conj #{})
80 | instance)
81 | ident (update-in
82 | [:com.fulcrologic.fulcro.application/indexes :ident->components ident]
83 | (fnil conj #{})
84 | instance))))
85 |
86 | (defn index-component!
87 | "Add a component instance to the app index. This adds the component to the `class->components` and
88 | `ident->components` indexes."
89 | [this]
90 | (let [{:keys [:com.fulcrologic.fulcro.application/runtime-atom]} (comp/any->app this)
91 | get-ident (comp/component-options this :ident)]
92 | (let [ident (when get-ident (get-ident this (comp/props this)))
93 | cls (comp/react-type this)]
94 | (when #?(:cljs goog.DEBUG :clj true)
95 | (when (and ident (not (eql/ident? ident)))
96 | (log/error "Component" (comp/component-name this) "supplied an invalid ident" ident))
97 | (when (and ident (nil? (second ident)))
98 | (log/info
99 | (str "component " (comp/component-name this) "'s ident (" ident ") has a `nil` second element."
100 | " This warning can be safely ignored if that is intended.")))
101 | (log/debug "Adding" (comp/component-name this) "instance to class index")
102 | (when ident
103 | (log/debug "Adding" (comp/component-name this) "with ident" ident "to ident index")))
104 | (swap! runtime-atom index-component* this ident cls))))
105 |
106 | (defn- drop-component*
107 | [runtime-state instance ident cls]
108 | (let [k (comp/class->registry-key cls)]
109 | (cond-> (update-in runtime-state [:com.fulcrologic.fulcro.application/indexes :class->components k]
110 | disj instance)
111 | ident (update-in
112 | [:com.fulcrologic.fulcro.application/indexes :ident->components ident]
113 | disj
114 | instance))))
115 |
116 | (defn drop-component!
117 | "Remove the component instance from the indexes. If ident is supplied it uses that, otherwise it gets the
118 | ident from the component itself."
119 | ([this ident]
120 | (let [{:keys [:com.fulcrologic.fulcro.application/runtime-atom]} (comp/any->app this)
121 | cls (comp/react-type this)]
122 | (log/debug "Dropping component instance with ident " ident "from indexes")
123 | (swap! runtime-atom drop-component* this ident cls)))
124 | ([this]
125 | (let [old-ident (comp/get-ident this)]
126 | (drop-component! this old-ident))))
127 |
--------------------------------------------------------------------------------
/src/test/com/fulcrologic/fulcro/algorithms/data_targeting_spec.clj:
--------------------------------------------------------------------------------
1 | (ns com.fulcrologic.fulcro.algorithms.data-targeting-spec
2 | (:require
3 | [com.fulcrologic.fulcro.algorithms.data-targeting :as targeting]
4 | [com.fulcrologic.fulcro.components :refer [defsc]]
5 | [fulcro-spec.core :refer
6 | [specification behavior assertions provided component when-mocking]]))
7 |
8 | (defsc A [_ _])
9 |
10 | (specification "Special targeting"
11 | (assertions
12 | "Reports false for non-special targets"
13 | (targeting/special-target? [:class]) => false
14 | (targeting/special-target? [::class]) => false
15 | "Allows for arbitrary metadata on the target"
16 | (targeting/special-target? (with-meta [::x :y :z] {:class A})) => false
17 | "Tolerates an empty target"
18 | (targeting/special-target? nil) => false
19 | "Is detectable"
20 | (targeting/special-target? (targeting/append-to [])) => true
21 | (targeting/special-target? (targeting/prepend-to [])) => true
22 | (targeting/special-target? (targeting/replace-at [])) => true
23 | (targeting/special-target? (targeting/multiple-targets [:a] [:b])) => true)
24 | (component "process-target"
25 | (let [starting-state {:root/thing [:y 2]
26 | :table {1 {:id 1 :thing [:x 1]}}}
27 | starting-state-many {:root/thing [[:y 2]]
28 | :table {1 {:id 1 :things [[:x 1] [:x 2]]}}}
29 | starting-state-data {:root/thing {:some "data"}
30 | :table {1 {:id 1 :things [{:foo "bar"}]}}}]
31 | (component "non-special targets"
32 | (assertions
33 | "moves an ident at some top-level key to an arbitrary path (non-special)"
34 | (targeting/process-target starting-state :root/thing [:table 1 :thing]) => {:table {1 {:id 1 :thing [:y 2]}}}
35 | "creates an ident for some location at a target location (non-special)"
36 | (targeting/process-target starting-state [:table 1] [:root/thing]) => {:root/thing [:table 1]
37 | :table {1 {:id 1 :thing [:x 1]}}}
38 | "replaces a to-many with a to-many (non-special)"
39 | (targeting/process-target starting-state-many :root/thing [:table 1 :things]) => {:table {1 {:id 1 :things [[:y 2]]}}}))
40 | (component "special targets"
41 | (assertions
42 | "can prepend into a to-many"
43 | (targeting/process-target starting-state-many :root/thing (targeting/prepend-to [:table 1 :things])) => {:table {1 {:id 1 :things [[:y 2] [:x 1] [:x 2]]}}}
44 | "can append into a to-many"
45 | (targeting/process-target starting-state-many :root/thing (targeting/append-to [:table 1 :things])) => {:table {1 {:id 1 :things [[:x 1] [:x 2] [:y 2]]}}}
46 | "keep data on db when remove-ok? is false"
47 | (targeting/process-target starting-state-data :root/thing (targeting/prepend-to [:table 1 :things]) false)
48 | => {:root/thing {:some "data"}
49 | :table {1 {:id 1 :things [{:some "data"} {:foo "bar"}]}}}
50 | ; Unsupported:
51 | ;"can replace an element in a to-many"
52 | ;(targeting/process-target starting-state-many :root/thing (targeting/replace-at [:table 1 :things 0])) => {:table {1 {:id 1 :things [[:y 2] [:x 2]]}}}
53 | "can affect multiple targets"
54 | (targeting/process-target starting-state-many :root/thing (targeting/multiple-targets
55 | (targeting/prepend-to [:table 1 :stuff])
56 | [:root/spot]
57 | (targeting/append-to [:table 1 :things]))) => {:root/spot [[:y 2]]
58 | :table {1 {:id 1
59 | :stuff [[:y 2]]
60 | :things [[:x 1] [:x 2] [:y 2]]}}})))))
61 |
62 | (specification "integrate-ident*"
63 | (let [state {:a {:path [[:table 2]]}
64 | :b {:path [[:table 2]]}
65 | :d [:table 6]
66 | :many {:path [[:table 99] [:table 88] [:table 77]]}}]
67 | (assertions
68 | "Can append to an existing vector"
69 | (-> state
70 | (targeting/integrate-ident* [:table 3] :append [:a :path])
71 | (get-in [:a :path]))
72 | => [[:table 2] [:table 3]]
73 |
74 | "Will append (create) on a non-existent vector"
75 | (-> state
76 | (targeting/integrate-ident* [:table 3] :append [:a :missing])
77 | (get-in [:a :missing]))
78 | => [[:table 3]]
79 |
80 | "(is a no-op if the ident is already there)"
81 | (-> state
82 | (targeting/integrate-ident* [:table 3] :append [:a :path])
83 | (get-in [:a :path]))
84 | => [[:table 2] [:table 3]]
85 |
86 | "Can prepend to an existing vector"
87 | (-> state
88 | (targeting/integrate-ident* [:table 3] :prepend [:b :path])
89 | (get-in [:b :path]))
90 | => [[:table 3] [:table 2]]
91 |
92 | "Will prepend (create) on a non-existent vector"
93 | (-> state
94 | (targeting/integrate-ident* [:table 3] :prepend [:a :missing])
95 | (get-in [:a :missing]))
96 | => [[:table 3]]
97 |
98 | "(is a no-op if already there)"
99 | (-> state
100 | (targeting/integrate-ident* [:table 3] :prepend [:b :path])
101 | (get-in [:b :path]))
102 | => [[:table 3] [:table 2]]
103 |
104 | "Can create/replace a to-one ident"
105 | (-> state
106 | (targeting/integrate-ident* [:table 3] :replace [:d])
107 | (get-in [:d]))
108 | => [:table 3]
109 | (-> state
110 | (targeting/integrate-ident* [:table 3] :replace [:c :path])
111 | (get-in [:c :path]))
112 | => [:table 3]
113 |
114 | "Can replace an existing to-many element in a vector"
115 | (-> state
116 | (targeting/integrate-ident* [:table 3] :replace [:many :path 1])
117 | (get-in [:many :path]))
118 | => [[:table 99] [:table 3] [:table 77]])))
119 |
--------------------------------------------------------------------------------
/src/main/com/fulcrologic/fulcro/dom.clj:
--------------------------------------------------------------------------------
1 | (ns com.fulcrologic.fulcro.dom
2 | "MACROS for generating CLJS code. See dom.cljs"
3 | (:refer-clojure :exclude [map meta time mask select use set symbol filter])
4 | (:require
5 | [clojure.spec.alpha :as s]
6 | [com.fulcrologic.fulcro.algorithms.do-not-use :as util]
7 | [com.fulcrologic.fulcro.components :as comp]
8 | [com.fulcrologic.fulcro.dom-common :as cdom]
9 | [clojure.string :as str])
10 | (:import
11 | (cljs.tagged_literals JSValue)
12 | (clojure.lang ExceptionInfo)))
13 |
14 | (defn- map-of-literals? [v]
15 | (and (map? v) (not-any? symbol? (tree-seq #(or (map? %) (vector? %) (seq? %)) seq v))))
16 | (s/def ::map-of-literals map-of-literals?)
17 |
18 | (defn- map-with-expr? [v]
19 | (and (map? v) (some #(or (symbol? %) (list? %)) (tree-seq #(or (map? %) (vector? %) (seq? %)) seq v))))
20 | (s/def ::map-with-expr map-with-expr?)
21 |
22 | (s/def ::dom-macro-args
23 | (s/cat
24 | :css (s/? keyword?)
25 | :attrs (s/? (s/or :nil nil?
26 | :map ::map-of-literals
27 | :runtime-map ::map-with-expr
28 | :js-object #(instance? JSValue %)
29 | :expression list?
30 | :symbol symbol?))
31 | :children (s/* (s/or :string string?
32 | :number number?
33 | :symbol symbol?
34 | :nil nil?
35 | :list sequential?))))
36 |
37 | (defn clj-map->js-object
38 | "Recursively convert a map to a JS object. For use in macro expansion."
39 | [m]
40 | {:pre [(map? m)]}
41 | (JSValue. (into {}
42 | (clojure.core/map (fn [[k v]]
43 | (cond
44 | (map? v) [k (clj-map->js-object v)]
45 | (vector? v) [k (mapv #(if (map? %) (clj-map->js-object %) %) v)]
46 | (symbol? v) [k `(cljs.core/clj->js ~v)]
47 | :else [k v])))
48 | m)))
49 |
50 | (defn emit-tag
51 | "PRIVATE. DO NOT USE.
52 |
53 | Helper function for generating CLJS DOM macros. is public for code gen problems."
54 | [str-tag-name args]
55 | (let [conformed-args (util/conform! ::dom-macro-args args)
56 | {attrs :attrs
57 | children :children
58 | css :css} conformed-args
59 | css-props (cdom/add-kwprops-to-props {} css)
60 | children (mapv (fn [[_ c]]
61 | (if (or (nil? c) (string? c))
62 | c
63 | `(comp/force-children ~c))) children)
64 | attrs-type (or (first attrs) :nil) ; attrs omitted == nil
65 | attrs-value (or (second attrs) {})
66 | create-element (case str-tag-name
67 | "input" 'com.fulcrologic.fulcro.dom/macro-create-wrapped-form-element
68 | "textarea" 'com.fulcrologic.fulcro.dom/macro-create-wrapped-form-element
69 | "select" 'com.fulcrologic.fulcro.dom/macro-create-wrapped-form-element
70 | "option" 'com.fulcrologic.fulcro.dom/macro-create-wrapped-form-element
71 | 'com.fulcrologic.fulcro.dom/macro-create-element*)
72 | classes-expression? (and (= attrs-type :map) (contains? attrs-value :classes))
73 | attrs-type (if classes-expression? :runtime-map attrs-type)]
74 | (case attrs-type
75 | :js-object ; kw combos not supported
76 | (if css
77 | (let [attr-expr `(cdom/add-kwprops-to-props ~attrs-value ~css)]
78 | `(~create-element ~(JSValue. (into [str-tag-name attr-expr] children))))
79 | `(~create-element ~(JSValue. (into [str-tag-name attrs-value] children))))
80 |
81 | :map
82 | `(~create-element ~(JSValue. (into [str-tag-name (-> attrs-value
83 | (cdom/add-kwprops-to-props css)
84 | (clj-map->js-object))]
85 | children)))
86 |
87 | :runtime-map
88 | `(com.fulcrologic.fulcro.dom/macro-create-element ~str-tag-name ~(into [attrs-value] children) ~css)
89 |
90 |
91 | (:symbol :expression)
92 | `(com.fulcrologic.fulcro.dom/macro-create-element
93 | ~str-tag-name ~(into [attrs-value] children) ~css)
94 |
95 | :nil
96 | `(~create-element
97 | ~(JSValue. (into [str-tag-name (JSValue. css-props)] children)))
98 |
99 | ;; pure children
100 | `(com.fulcrologic.fulcro.dom/macro-create-element
101 | ~str-tag-name ~(JSValue. (into [attrs-value] children)) ~css))))
102 |
103 | (defn syntax-error
104 | "Format a DOM syntax error"
105 | [and-form ex]
106 | (let [location (clojure.core/meta and-form)
107 | file (some-> (:file location) (str/replace #".*[/]" ""))
108 | line (:line location)
109 | unexpected-input (::s/value (ex-data ex))]
110 | (str "Syntax error at " file ":" line ". Unexpected input " unexpected-input)))
111 |
112 | (defn gen-dom-macro [emitter name]
113 | `(defmacro ~name ~(cdom/gen-docstring name true)
114 | [& ~'args]
115 | (let [tag# ~(str name)]
116 | (try
117 | (~emitter tag# ~'args)
118 | (catch ExceptionInfo e#
119 | (throw (ex-info (syntax-error ~'&form e#) (ex-data e#))))))))
120 |
121 | (defmacro gen-dom-macros [emitter]
122 | `(do ~@(clojure.core/map (partial gen-dom-macro emitter) cdom/tags)))
123 |
124 | (defn- gen-client-dom-fn [create-element-symbol tag]
125 | `(defn ~tag ~(cdom/gen-docstring tag true)
126 | [& ~'args]
127 | (let [conformed-args# (util/conform! :com.fulcrologic.fulcro.dom/dom-element-args ~'args) ; see CLJS file for spec
128 | {attrs# :attrs
129 | children# :children
130 | css# :css} conformed-args#
131 | children# (mapv second children#)
132 | attrs-value# (or (second attrs#) {})]
133 | (~create-element-symbol ~(name tag) (into [attrs-value#] children#) css#))))
134 |
135 | (defmacro gen-client-dom-fns [create-element-sym]
136 | `(do ~@(clojure.core/map (partial gen-client-dom-fn create-element-sym) cdom/tags)))
137 |
138 | (gen-dom-macros com.fulcrologic.fulcro.dom/emit-tag)
139 |
--------------------------------------------------------------------------------
/src/main/com/fulcrologic/fulcro/dom/html_entities.cljc:
--------------------------------------------------------------------------------
1 | (ns com.fulcrologic.fulcro.dom.html-entities
2 | "Defs of the proper unicode characters so you can use html entities in your DOM functions without having to
3 | look them up (or type out silly things like \"\\u00C6\"
4 |
5 | For example:
6 |
7 | ```
8 | (ns my.ui
9 | (:require
10 | [com.fulcrologic.fulcro.dom.html-entities :as ent]))
11 |
12 | ...
13 | (dom/div ent/nbsp ent/copy)
14 | ...
15 | ```
16 | "
17 | (:refer-clojure :exclude [quot divide and or not empty int]))
18 |
19 | (def AElig "\u00C6")
20 | (def Aacute "\u00C1")
21 | (def Acirc "\u00C2")
22 | (def Agrave "\u00C0")
23 | (def Alpha "\u0391")
24 | (def Aring "\u00C5")
25 | (def Atilde "\u00C3")
26 | (def Auml "\u00C4")
27 | (def Beta "\u0392")
28 | (def Ccedil "\u00C7")
29 | (def Chi "\u03A7")
30 | (def Dagger "\u2021")
31 | (def Delta "\u0394")
32 | (def ETH "\u00D0")
33 | (def Eacute "\u00C9")
34 | (def Ecirc "\u00CA")
35 | (def Egrave "\u00C8")
36 | (def Epsilon "\u0395")
37 | (def Eta "\u0397")
38 | (def Euml "\u00CB")
39 | (def Gamma "\u0393")
40 | (def Iacute "\u00CD")
41 | (def Icirc "\u00CE")
42 | (def Igrave "\u00CC")
43 | (def Iota "\u0399")
44 | (def Iuml "\u00CF")
45 | (def Kappa "\u039A")
46 | (def Lambda "\u039B")
47 | (def Mu "\u039C")
48 | (def Ntilde "\u00D1")
49 | (def Nu "\u039D")
50 | (def OElig "\u0152")
51 | (def Oacute "\u00D3")
52 | (def Ocirc "\u00D4")
53 | (def Ograve "\u00D2")
54 | (def Omega "\u03A9")
55 | (def Omicron "\u039F")
56 | (def Oslash "\u00D8")
57 | (def Otilde "\u00D5")
58 | (def Ouml "\u00D6")
59 | (def Phi "\u03A6")
60 | (def Pi "\u03A0")
61 | (def Prime "\u2033")
62 | (def Psi "\u03A8")
63 | (def Rho "\u03A1")
64 | (def Scaron "\u0160")
65 | (def Sigma "\u03A3")
66 | (def THORN "\u00DE")
67 | (def Tau "\u03A4")
68 | (def Theta "\u0398")
69 | (def Uacute "\u00DA")
70 | (def Ucirc "\u00DB")
71 | (def Ugrave "\u00D9")
72 | (def Upsilon "\u03A5")
73 | (def Uuml "\u00DC")
74 | (def Xi "\u039E")
75 | (def Yacute "\u00DD")
76 | (def Yuml "\u0178")
77 | (def Zeta "\u0396")
78 | (def aacute "\u00E1")
79 | (def acirc "\u00E2")
80 | (def acute "\u00B4")
81 | (def aelig "\u00E6")
82 | (def agrave "\u00E0")
83 | (def alefsym "\u2135")
84 | (def alpha "\u03B1")
85 | (def amp "\u0026")
86 | (def and "\u2227")
87 | (def ang "\u2220")
88 | (def apos "\u0027")
89 | (def aring "\u00E5")
90 | (def asymp "\u2248")
91 | (def atilde "\u00E3")
92 | (def auml "\u00E4")
93 | (def bdquo "\u201E")
94 | (def beta "\u03B2")
95 | (def brvbar "\u00A6")
96 | (def bull "\u2022")
97 | (def cap "\u2229")
98 | (def ccedil "\u00E7")
99 | (def cedil "\u00B8")
100 | (def cent "\u00A2")
101 | (def chi "\u03C7")
102 | (def circ "\u02C6")
103 | (def clubs "\u2663")
104 | (def cong "\u2245")
105 | (def copy "\u00A9")
106 | (def crarr "\u21B5")
107 | (def cup "\u222A")
108 | (def curren "\u00A4")
109 | (def dArr "\u21D3")
110 | (def dagger "\u2020")
111 | (def darr "\u2193")
112 | (def deg "\u00B0")
113 | (def delta "\u03B4")
114 | (def diams "\u2666")
115 | (def divide "\u00F7")
116 | (def eacute "\u00E9")
117 | (def ecirc "\u00EA")
118 | (def egrave "\u00E8")
119 | (def empty "\u2205")
120 | (def emsp "\u2003")
121 | (def ensp "\u2002")
122 | (def epsilon "\u03B5")
123 | (def equiv "\u2261")
124 | (def eta "\u03B7")
125 | (def eth "\u00F0")
126 | (def euml "\u00EB")
127 | (def euro "\u20AC")
128 | (def exist "\u2203")
129 | (def fnof "\u0192")
130 | (def forall "\u2200")
131 | (def frac12 "\u00BD")
132 | (def frac14 "\u00BC")
133 | (def frac34 "\u00BE")
134 | (def frasl "\u2044")
135 | (def gamma "\u03B3")
136 | (def ge "\u2265")
137 | (def gt "\u003E")
138 | (def hArr "\u21D4")
139 | (def harr "\u2194")
140 | (def hearts "\u2665")
141 | (def hellip "\u2026")
142 | (def horbar "\u2015")
143 | (def iacute "\u00ED")
144 | (def icirc "\u00EE")
145 | (def iexcl "\u00A1")
146 | (def igrave "\u00EC")
147 | (def image "\u2111")
148 | (def infin "\u221E")
149 | (def int "\u222B")
150 | (def iota "\u03B9")
151 | (def iquest "\u00BF")
152 | (def isin "\u2208")
153 | (def iuml "\u00EF")
154 | (def kappa "\u03BA")
155 | (def lArr "\u21D0")
156 | (def lambda "\u03BB")
157 | (def lang "\u2329")
158 | (def laqao "\u00AB") ; sic
159 | (def laquo "\u00AB")
160 | (def larr "\u2190")
161 | (def lceil "\u2308")
162 | (def ldquo "\u201C")
163 | (def le "\u2264")
164 | (def lfloor "\u230A")
165 | (def lowast "\u2217")
166 | (def loz "\u25CA")
167 | (def lrm "\u200E")
168 | (def lsaquo "\u2039")
169 | (def lsquo "\u2018")
170 | (def lt "\u003C")
171 | (def macr "\u00AF")
172 | (def mdash "\u2014")
173 | (def micro "\u00B5")
174 | (def middot "\u00B7")
175 | (def minus "\u2212")
176 | (def mu "\u03BC")
177 | (def nabla "\u2207")
178 | (def nbsp "\u00A0")
179 | (def ndash "\u2013")
180 | (def ne "\u2260")
181 | (def ni "\u220B")
182 | (def not "\u00AC")
183 | (def notin "\u2209")
184 | (def nsub "\u2284")
185 | (def ntilde "\u00F1")
186 | (def nu "\u03BD")
187 | (def oacute "\u00F3")
188 | (def ocirc "\u00F4")
189 | (def oelig "\u0153")
190 | (def ograve "\u00F2")
191 | (def oline "\u203E")
192 | (def omega "\u03C9")
193 | (def omicron "\u03BF")
194 | (def oplus "\u2295")
195 | (def or "\u2228")
196 | (def ordf "\u00AA")
197 | (def ordm "\u00BA")
198 | (def oslash "\u00F8")
199 | (def otilde "\u00F5")
200 | (def otimes "\u2297")
201 | (def ouml "\u00F6")
202 | (def para "\u00B6")
203 | (def part "\u2202")
204 | (def permil "\u2030")
205 | (def perp "\u22A5")
206 | (def phi "\u03C6")
207 | (def pi "\u03C0")
208 | (def piv "\u03D6")
209 | (def plusmn "\u00B1")
210 | (def pound "\u00A3")
211 | (def prime "\u2032")
212 | (def prod "\u220F")
213 | (def prop "\u221D")
214 | (def psi "\u03C8")
215 | (def quot "\u0022")
216 | (def rArr "\u21D2")
217 | (def radic "\u221A")
218 | (def rang "\u232A")
219 | (def raqao "\u00BB") ; sic
220 | (def raquo "\u00BB")
221 | (def rarr "\u2192")
222 | (def rceil "\u2309")
223 | (def rdquo "\u201D")
224 | (def real "\u211C")
225 | (def reg "\u00AE")
226 | (def rfloor "\u230B")
227 | (def rho "\u03C1")
228 | (def rlm "\u200F")
229 | (def rsaquo "\u203A")
230 | (def rsquo "\u2019")
231 | (def sbquo "\u201A")
232 | (def scaron "\u0161")
233 | (def sdot "\u22C5")
234 | (def sect "\u00A7")
235 | (def shy "\u00AD")
236 | (def sigma "\u03C3")
237 | (def sigmaf "\u03C2")
238 | (def sim "\u223C")
239 | (def spades "\u2660")
240 | (def sub "\u2282")
241 | (def sube "\u2286")
242 | (def sum "\u2211")
243 | (def sup "\u2283")
244 | (def sup1 "\u00B9")
245 | (def sup2 "\u00B2")
246 | (def sup3 "\u00B3")
247 | (def supe "\u2287")
248 | (def szlig "\u00DF")
249 | (def tau "\u03C4")
250 | (def there4 "\u2234")
251 | (def theta "\u03B8")
252 | (def thetasym "\u03D1")
253 | (def thinsp "\u2009")
254 | (def thorn "\u00FE")
255 | (def tilde "\u02DC")
256 | (def times "\u00D7")
257 | (def trade "\u2122")
258 | (def uArr "\u21D1")
259 | (def uacute "\u00FA")
260 | (def uarr "\u2191")
261 | (def ucirc "\u00FB")
262 | (def ugrave "\u00F9")
263 | (def uml "\u00A8")
264 | (def upsih "\u03D2")
265 | (def upsilon "\u03C5")
266 | (def uuml "\u00FC")
267 | (def weierp "\u2118")
268 | (def xi "\u03BE")
269 | (def yacute "\u00FD")
270 | (def yen "\u00A5")
271 | (def yuml "\u00FF")
272 | (def zeta "\u03B6")
273 | (def zwj "\u200D")
274 | (def zwnj "\u200C")
275 |
--------------------------------------------------------------------------------
/src/test/com/fulcrologic/fulcro/algorithms/normalize_spec.cljc:
--------------------------------------------------------------------------------
1 | (ns com.fulcrologic.fulcro.algorithms.normalize-spec
2 | (:require
3 | #?(:clj [clojure.test :refer :all]
4 | :cljs [clojure.test :refer [deftest]])
5 | [com.fulcrologic.fulcro.components :as comp :refer [defsc]]
6 | [fulcro-spec.core :refer [assertions specification component when-mocking behavior]]
7 | [com.fulcrologic.fulcro.algorithms.merge :as merge]
8 | [com.fulcrologic.fulcro.algorithms.denormalize :as fdn]
9 | [com.fulcrologic.fulcro.algorithms.normalize :as fnorm]
10 | [com.fulcrologic.fulcro.algorithms.do-not-use :as util]
11 | [com.fulcrologic.fulcro.application :as app]))
12 |
13 | (defsc A [this props])
14 | (defsc AQuery [this props] {:query [:x]})
15 | (defsc AState [this props] {:initial-state (fn [params] {})})
16 | (defsc AIdent [this props] {:ident (fn [] [:x 1])})
17 | (defsc APreMerge [this props] {:pre-merge (fn [_])})
18 |
19 | (defsc AUIChild [_ _] {:ident [:ui/id :ui/id]
20 | :query [:ui/id :ui/name]
21 | :pre-merge (fn [{:keys [current-normalized data-tree]}]
22 | (merge
23 | {:ui/id "child-id"
24 | :ui/name "123"}
25 | current-normalized data-tree))})
26 |
27 | (defsc AUIChildWithoutPreMerge [_ _]
28 | {:ident [:ui/id :ui/id]
29 | :query [:ui/id :ui/name]})
30 |
31 | (defsc AUIParent [_ _] {:ident [:id :id]
32 | :query [:id {:ui/child (comp/get-query AUIChild)}]
33 | :pre-merge (fn [{:keys [current-normalized data-tree]}]
34 | (merge
35 | {:ui/child {}}
36 | current-normalized
37 | data-tree))})
38 |
39 | (defn- build-simple-ident [ident props]
40 | (if (fn? ident)
41 | (ident props)
42 | [ident (get props ident)]))
43 |
44 | (defn- quick-ident-class [ident]
45 | #?(:cljs (comp/configure-component! (fn []) :A
46 | {:ident (fn [_ props] (build-simple-ident ident props))})
47 | :clj (comp/configure-component! {} :A
48 | {:ident (fn [_ props] (build-simple-ident ident props))})))
49 |
50 | (defn- genc [ident query]
51 | (with-meta query
52 | {:component (quick-ident-class ident)}))
53 |
54 | (defn- ident-from-prop [available]
55 | (fn [props]
56 | (or (some #(if-let [x (get props %)] [% x]) available)
57 | [:unknown nil])))
58 |
59 |
60 | (specification "tree->db"
61 | (assertions
62 | "[*]"
63 | (fnorm/tree->db ['*] {:foo "bar"})
64 | => {:foo "bar"})
65 |
66 | (assertions
67 | "reading properties"
68 | (fnorm/tree->db [:a] {:a 1 :z 10})
69 | => {:a 1, :z 10})
70 |
71 | (assertions
72 | "union case"
73 | (fnorm/tree->db [{:multi (genc
74 | (ident-from-prop [:a/id :b/id])
75 | {:a (genc :a/id [:a/id :a/name])
76 | :b (genc :b/id [:b/id :a/name])})}]
77 | {:multi {:a/id 3}})
78 | => {:multi [:a/id 3]}
79 |
80 | (fnorm/tree->db [{:multi (genc
81 | (ident-from-prop [:a/id :b/id])
82 | {:a (genc :a/id [:a/id :a/name])
83 | :b (genc :b/id [:b/id :a/name])})}]
84 | {:multi {:b/id 5}} true)
85 | => {:multi [:b/id 5]
86 | :b/id {5 #:b{:id 5}}}
87 |
88 | (fnorm/tree->db [{:multi (genc
89 | (ident-from-prop [:a/id :b/id])
90 | {:a (genc :a/id [:a/id :a/name])
91 | :b (genc :b/id [:b/id :a/name])})}]
92 | {:multi [{:b/id 3}
93 | {:c/id 5}
94 | {:a/id 42}]} true)
95 | => {:multi [[:b/id 3] [:unknown nil] [:a/id 42]]
96 | :b/id {3 #:b{:id 3}}
97 | :unknown {nil #:c{:id 5}}
98 | :a/id {42 #:a{:id 42}}})
99 |
100 | (assertions
101 | "union case missing ident"
102 | (fnorm/tree->db [{:multi {:a (genc :a/id [:a/id :a/name])
103 | :b (genc :b/id [:b/id :a/name])}}]
104 | {:multi {:a/id 3}})
105 | =throws=> #"Union components must have an ident")
106 |
107 | (assertions
108 | "normalized data"
109 | (fnorm/tree->db [{:foo (genc :id [:id])}] {:foo [:id 123]} true)
110 | => {:foo [:id 123]})
111 |
112 | (assertions
113 | "to one join"
114 | (fnorm/tree->db [{:foo (genc :id [:id])}] {:foo {:id 123 :x 42}} true)
115 | => {:foo [:id 123]
116 | :id {123 {:id 123, :x 42}}}
117 |
118 | (fnorm/tree->db [{:foo (genc :id [:id])}] {:foo {:x 42}} true)
119 | => {:foo [:id nil],
120 | :id {nil {:x 42}}}
121 |
122 | (fnorm/tree->db [{:foo (genc :id [:id])}] {:bar {:id 123 :x 42}} true)
123 | => {:bar {:id 123, :x 42}})
124 |
125 | (assertions
126 | "to many join"
127 | (fnorm/tree->db [{:foo (genc :id [:id])}] {:foo [{:id 1 :x 42}
128 | {:id 2}]} true)
129 | => {:foo [[:id 1] [:id 2]], :id {1 {:id 1, :x 42}, 2 {:id 2}}})
130 |
131 | (assertions
132 | "bounded recursive query"
133 | (fnorm/tree->db [{:root (genc :id [:id {:p 2}])}]
134 | {:root {:id 1 :p {:id 2 :p {:id 3 :p {:id 4 :p {:id 5}}}}}} true)
135 | => {:root [:id 1]
136 | :id {5 {:id 5}
137 | 4 {:id 4, :p [:id 5]}
138 | 3 {:id 3, :p [:id 4]}
139 | 2 {:id 2, :p [:id 3]}
140 | 1 {:id 1, :p [:id 2]}}})
141 |
142 | (assertions
143 | "unbounded recursive query"
144 | (fnorm/tree->db [{:root (genc :id [:id {:p '...}])}]
145 | {:root {:id 1 :p {:id 2 :p {:id 3 :p {:id 4 :p {:id 5}}}}}} true)
146 | => {:root [:id 1]
147 | :id {5 {:id 5}
148 | 4 {:id 4, :p [:id 5]}
149 | 3 {:id 3, :p [:id 4]}
150 | 2 {:id 2, :p [:id 3]}
151 | 1 {:id 1, :p [:id 2]}}})
152 |
153 | (behavior "using with pre-merge-transform"
154 | (assertions
155 | (fnorm/tree->db AUIParent {:id 123} true (merge/pre-merge-transform {}))
156 | => {:id 123
157 | :ui/child [:ui/id "child-id"]
158 | :ui/id {"child-id" {:ui/id "child-id", :ui/name "123"}}}
159 |
160 | "to one idents"
161 | (fnorm/tree->db AUIParent {:id 123} true
162 | (merge/pre-merge-transform {:id {123 {:id 123
163 | :ui/child [:ui/id "child-id"]}}
164 | :ui/id {"child-id" {:ui/id "child-id", :ui/name "123"}}}))
165 | => {:id 123
166 | :ui/child [:ui/id "child-id"]}
167 |
168 | "to many idents"
169 | (fnorm/tree->db AUIParent {:id 123} true
170 | (merge/pre-merge-transform {:id {123 {:id 123
171 | :ui/child [[:ui/id "child-id"]
172 | [:ui/id "child-id2"]]}}
173 | :ui/id {"child-id" {:ui/id "child-id", :ui/name "123"}
174 | "child-id2" {:ui/id "child-id2", :ui/name "456"}}}))
175 | => {:id 123
176 | :ui/child [[:ui/id "child-id"]
177 | [:ui/id "child-id2"]]})))
178 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | 3.0.0-beta-1
2 | ------------
3 | - Downgraded ghostwheel. This makes cljs builds bigger, but was necessary because
4 | newer gw is still SNAPSHOT
5 | - Cleaned up a number of functions that needed to not move forward.
6 | - Removed legacy transit overrides in middleware. This means CLJS side will receive
7 | tagged bigdecimals now instead of floats. This can still be done for bw compat
8 | with apps that want it, but should not be the default for the library.
9 |
10 | 3.0.0-alpha-22
11 | --------------
12 | - Working on final naming for APIs.
13 | - NAME UPDATES (BREAKING)
14 | - namespace application-helpers -> lookup
15 | - api-middleware: augment-map -> apply-response-augmentations
16 |
17 | 3.0.0-alpha-21
18 | --------------
19 | - Added support for network aborts
20 | - Fixed pre-merge support in merge-component
21 | - Reworked merge-component! to use merge-component
22 | - Added support for alternate default config edn
23 | - Fixed legacy union router ident signature
24 | - Fixed dynamic routing path interpretation
25 | - Made mark/sweep merge an option of general merge functions
26 |
27 | 3.0.0-alpha-20
28 | --------------
29 | - Added hooks to redefine render/hydrate (for native support)
30 |
31 | 3.0.0-alpha-19
32 | --------------
33 | - Removed :constructor
34 | - Added props as arg to `:initLocalState`
35 | - Fixed bug from incubator commit 38659c19cc8caa20167d4649242039d7a35dfae1
36 | - Fixed/updated dynamic router
37 | - Fixed some ident-optimized render oversights
38 | - Made it possible to self-refer in a mutation body
39 |
40 | 3.0.0-alpha-18
41 | --------------
42 | - Various SSR fixes
43 | - Fixed initial state def and use. Was inconsistent in some places.
44 | - Added default mutation result handler update [ref ::m/mutation-error]
45 | - Made default mutation result handler a re-usable composition.
46 | - Made each step of default result handler a reusable functional step.
47 | - BREAKING CHANGE: `default-result-action` renamed to `default-result-action!`
48 | - Updates/fixes for CCI/cljdoc
49 |
50 | 3.0.0-alpha-17
51 | --------------
52 | - Added active remote status tracking to state atom
53 | - Added compressible transactions with support in Inspect
54 | - Reduced chances of lost db updates for inspect.
55 | - Added hooks to allow global customization of load internals
56 | - Added global query transform override
57 | - Added load marker default support
58 | - Fixed http remote to not force status code to 500 unless it is missing on errors.
59 | - Removed targeting aliases in data-fetch. Use the targeting ns directly for append-to et al.
60 | - Fixed problem with mutations sending as empty-query mutation joins due
61 | to faulty global query transform.
62 | - Changed internal alg names so that they could be safely spec'd
63 |
64 | 3.0.0-alpha-16
65 | --------------
66 | - Added failed load marker support.
67 | - Fixed missed render on `set-query!`.
68 | - Added follow-on read support.
69 | - Updated render scheduling to be debounced instead of queued to avoid extra refreshes.
70 | - Fixed tracking of `:refresh` and `:only-refresh` to accumulate and clear properly.
71 | - Added back support for `refresh` section of mutations.
72 | - Added lost-refresh avoidance when both refresh and only-refresh collide.
73 | - Added recovery from failures in ident-optimized refresh with console messaging
74 | - Changed indexes to use class registry keys instead of classes so hot reload works better.
75 | - Fixed load fallbacks
76 |
77 | 3.0.0-alpha-15
78 | --------------
79 | - Fixed issue with new remote-error? override
80 |
81 | 3.0.0-alpha-14
82 | --------------
83 | - Added missing (deprecated) load/load-field
84 | - Added ability to override remote-error?
85 | - Removed busted specs in new remote
86 |
87 | 3.0.0-alpha-13
88 | --------------
89 | - Lots of doc string improvements
90 | - More conversions to ghostwheel
91 | - BREAKING: Changed ns name of union-router to legacy-ui-routers.
92 |
93 | 3.0.0-alpha-12
94 | --------------
95 | - Fixed issues with parallel load option
96 | - Fixed issue with pre-merge and initial app state
97 | - Added additional missing inspect support (dom preview, network, etc.)
98 |
99 | 3.0.0-alpha-11
100 | --------------
101 | - Fixed clj render to work well with css lib
102 | - Switched react back to cljsjs...no need for that break
103 |
104 | 3.0.0-alpha-10
105 | --------------
106 | - Added missing augment-response helper to mware
107 | - Fixed a couple of bugs in optimized render
108 | - Fixed bug in new db->tree
109 | - Added missing link query tsts for db->tree
110 | - Fixed bug in tx processing for remote false
111 | - Added more to html events ns
112 | - Updated specs for form-state
113 |
114 | 3.0.0-alpha-9
115 | -------------
116 | - Improved ident-optimized render, added support for :only-refresh
117 | - Added specs and docs strings
118 | - Fixed some naming where registry key should have been used
119 | - Fixed componentDidMount prev-props (failing to update)
120 | - Fixed tempid rewrites on mutation return from server
121 | - Changed transit stack. Updated dev guide for it.
122 | - Deprecated some names
123 | - Added SSR render-to-string support
124 | - Switched to ghostwheel 0.4 for better spec elision in production builds
125 |
126 | 3.0.0-alpha-8
127 | -------------
128 | - Improved logging helpers
129 | - Some UISM touch-ups
130 | - Minor fixes around mount error checks and debug logging
131 |
132 | 3.0.0-alpha-7
133 | -------------
134 | - Improved hot code reload (app root and dyn router)
135 | - Workaround gw bug
136 | - Made will-leave and route-cancelled optional on dr route targets
137 | - Did some minor renames in UI state machines.
138 | - A number of issues fixed in UI state machines that were caused by some
139 | renames.
140 |
141 | 3.0.0-alpha-6
142 | -------------
143 | - Added official hooks to a number of places, and added a bit to docs
144 | - Finished defining the "default" amount of pluggability for default mutations
145 | - Make global query transform apply closer to the network layer to catch both mutations and queries
146 | - Added confirmation tests to some more elements of merge join and pre-merge
147 |
148 | 3.0.0-alpha-5
149 | -------------
150 | - Fixed Fulcro Inspect db view (was missing deltas)
151 | - Added missing support for returning/with-params/with-target to mutations,
152 | but change remotes to allow `env` in addition to boolean and AST so that
153 | the threading of those functions are cleaner.
154 | - Added/modified how custom start-up state is given to an app.
155 | - Fixed rendering bug so that app normalization is optional (useful in small demos)
156 |
157 | 3.0.0-alpha-4
158 | -------------
159 | - Fixed dependency on EQL
160 |
161 | 3.0.0-alpha-3
162 | -------------
163 | - Fixed bug in tx processing
164 | - Fixed missing refresh in ident optimized render when using ident joins
165 | - Added missing indexes
166 | - Added various missing functions and namespaces
167 |
168 | 3.0.0-alpha-2
169 | -------------
170 | - Added Inspect preload so F3 can work with Chrome extension
171 | - Updated readme
172 | - Integrated many namespaces from F2: form-state, icons, events, entities...
173 | - Added numerous tests.
174 | - Added a few more missing utility functions
175 |
176 | 3.0.0-alpha-1
177 | -------------
178 | - Major APIs all ported, and somewhat tested
179 | - Websockets is in a separate library (reduced server deps)
180 | - Not API compatible with F2 yet. See todomvc sample source for basics
181 |
182 |
183 | 3.0.0-pre-alpha-5
184 | -----------------
185 | - Added shared back. Forcing root render and `update-shared!` updates shared-fn values. Slight operational difference from 2.x.
186 |
187 | 3.0.0-pre-alpha-4
188 | -----------------
189 | - Added real and mock http remote
190 | - Added server middleware
191 | - Ported (but untested) uism and routing
192 |
193 |
--------------------------------------------------------------------------------
/src/main/com/fulcrologic/fulcro/rendering/ident_optimized_render.cljc:
--------------------------------------------------------------------------------
1 | (ns com.fulcrologic.fulcro.rendering.ident-optimized-render
2 | "A render optimization algorithm for refreshing the UI via props tunnelling (setting new props on a component's
3 | state in a pre-agreed location). This algorithm analyzes database changes and on-screen components to update
4 | components (by ident) whose props have changed.
5 |
6 | Prop change detection is done by scanning the database in *only* the locations that on-screen components are querying
7 | (derived by the mounted component idents, and any ident-joins in the queries)."
8 | (:require
9 | [com.fulcrologic.fulcro.rendering.keyframe-render :as kr]
10 | [com.fulcrologic.fulcro.algorithms.denormalize :as fdn]
11 | [com.fulcrologic.fulcro.components :as comp]
12 | [clojure.set :as set]
13 | [edn-query-language.core :as eql]
14 | [taoensso.timbre :as log]))
15 |
16 | (defn dirty-table-entries
17 | "Checks the given `idents` and returns a subset of them where the data they refer to has changed
18 | between `old-state` and `new-state`."
19 | [old-state new-state idents]
20 | (reduce
21 | (fn [result ident]
22 | (if (identical? (get-in old-state ident) (get-in new-state ident))
23 | result
24 | (cons ident result)))
25 | (list)
26 | idents))
27 |
28 | (defn render-component!
29 | "Uses the component's query and the current application state to query for the current value of that component's
30 | props (subtree). It then sends those props to the component via \"props tunnelling\" (setting them on a well-known key in
31 | component-local state)."
32 | [app ident c]
33 | #?(:cljs
34 | (if (and c ident)
35 | (let [{:com.fulcrologic.fulcro.application/keys [state-atom]} app
36 | state-map @state-atom
37 | query (comp/get-query c state-map)
38 | q [{ident query}]
39 | data-tree (when query (fdn/db->tree q state-map state-map))
40 | new-props (get data-tree ident)]
41 | (when-not query (log/error "Query was empty. Refresh failed for " (type c)))
42 | (when (comp/mounted? c)
43 | (.setState ^js c (fn [s] #js {"fulcro$value" new-props}))))
44 | (do
45 | (log/info "Failed to do optimized update. Component" (-> c comp/react-type (comp/class->registry-key))
46 | "queries for data that changed, but does not have an ident. If that is your application root,"
47 | "consider moving that changing state to a child component.")
48 | (throw (ex-info "Targeted update failed" {}))))))
49 |
50 | (defn render-components-with-ident!
51 | "Renders *only* components that *have* the given ident."
52 | [app ident]
53 | (doseq [c (comp/ident->components app ident)]
54 | (render-component! app ident c)))
55 |
56 | (defn render-dependents-of-ident!
57 | "Renders components that have, or query for, the given ident."
58 | [app ident]
59 | (render-components-with-ident! app ident)
60 | (let [{:com.fulcrologic.fulcro.application/keys [runtime-atom]} app
61 | {:com.fulcrologic.fulcro.application/keys [indexes]} @runtime-atom
62 | {:keys [prop->classes idents-in-joins class->components]} indexes
63 | idents-in-joins (or idents-in-joins #{})]
64 | (when (contains? idents-in-joins ident)
65 | (let [classes (prop->classes ident)]
66 | (when (seq classes)
67 | (doseq [class classes]
68 | (doseq [component (class->components class)
69 | :let [component-ident (comp/get-ident component)]]
70 | (render-component! app component-ident component))))))))
71 |
72 | (defn props->components
73 | "Given an app and a `property-set`: returns the components that query for the items in property-set.
74 |
75 | The `property-set` can be any sequence (ideally a set) of keywords and idents that can directly appear
76 | in a component query as a property or join key."
77 | [app property-set]
78 | (when (seq property-set)
79 | (let [{:com.fulcrologic.fulcro.application/keys [runtime-atom]} app
80 | {:com.fulcrologic.fulcro.application/keys [indexes]} @runtime-atom
81 | {:keys [prop->classes class->components]} indexes]
82 | (reduce
83 | (fn [result prop]
84 | (let [classes (prop->classes prop)
85 | components (reduce #(into %1 (class->components %2)) #{} classes)]
86 | (into result components)))
87 | #{}
88 | property-set))))
89 |
90 | (defn render-stale-components!
91 | "This function tracks the state of the app at the time of prior render in the app's runtime-atom. It
92 | uses that to do a comparison of old vs. current application state (bounded by the needs of on-screen components).
93 | When it finds data that has changed it renders all of the components that depend on that data."
94 | [app]
95 | (let [{:com.fulcrologic.fulcro.application/keys [runtime-atom state-atom]} app
96 | {:com.fulcrologic.fulcro.application/keys [indexes last-rendered-state
97 | to-refresh only-refresh]} @runtime-atom
98 | {:keys [linked-props ident->components prop->classes idents-in-joins]} indexes
99 | limited-refresh? (seq only-refresh)]
100 | (if limited-refresh?
101 | (let [{limited-idents true
102 | limited-props false} (group-by eql/ident? only-refresh)
103 | limited-to-render (props->components app limited-props)]
104 | (doseq [c limited-to-render
105 | :let [ident (comp/get-ident c)]]
106 | (render-component! app ident c))
107 | (doseq [i limited-idents]
108 | (render-dependents-of-ident! app i)))
109 | (let [state-map @state-atom
110 | idents-in-joins (or idents-in-joins #{})
111 | dirty-linked-props (reduce
112 | (fn [acc p]
113 | (if (not (identical?
114 | (get state-map p)
115 | (get last-rendered-state p)))
116 | (conj acc p)
117 | acc))
118 | #{}
119 | linked-props)
120 | {idents-to-force true
121 | props-to-force false} (group-by eql/ident? to-refresh)
122 | props-to-force (set/union props-to-force dirty-linked-props)
123 | mounted-idents (concat (keys ident->components) idents-in-joins)
124 | stale-idents (dirty-table-entries last-rendered-state state-map mounted-idents)
125 | extra-to-force (props->components app props-to-force)
126 | all-idents (set/union (set idents-to-force) (set stale-idents))]
127 | (doseq [i all-idents]
128 | (render-dependents-of-ident! app i))
129 | (doseq [c extra-to-force]
130 | (render-component! app (comp/get-ident c) c))))))
131 |
132 | (defn render!
133 | "The top-level call for using this optimized render in your application.
134 |
135 | If `:force-root? true` is passed in options, then it just forces a keyframe root render; otherwise
136 | it tries to minimize the work done for screen refresh to just the queries/refreshes needed by the
137 | data that has changed."
138 | ([app]
139 | (render! app {}))
140 | ([app {:keys [force-root? root-props-changed?] :as options}]
141 | (if (or force-root? root-props-changed?)
142 | (kr/render! app options)
143 | (try
144 | (render-stale-components! app)
145 | (catch #?(:clj Exception :cljs :default) e
146 | (log/info "Optimized render failed. Falling back to root render.")
147 | (kr/render! app options))))))
148 |
149 |
--------------------------------------------------------------------------------
/resources/public/index.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | margin: 0;
4 | padding: 0;
5 | }
6 |
7 | button {
8 | margin: 0;
9 | padding: 0;
10 | border: 0;
11 | background: none;
12 | font-size: 100%;
13 | vertical-align: baseline;
14 | font-family: inherit;
15 | font-weight: inherit;
16 | color: inherit;
17 | -webkit-appearance: none;
18 | appearance: none;
19 | -webkit-font-smoothing: antialiased;
20 | -moz-osx-font-smoothing: grayscale;
21 | }
22 |
23 | body {
24 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
25 | line-height: 1.4em;
26 | background: #f5f5f5;
27 | color: #4d4d4d;
28 | min-width: 230px;
29 | max-width: 550px;
30 | margin: 0 auto;
31 | -webkit-font-smoothing: antialiased;
32 | -moz-osx-font-smoothing: grayscale;
33 | font-weight: 300;
34 | }
35 |
36 | button,
37 | input[type="checkbox"] {
38 | outline: none;
39 | }
40 |
41 | .hidden {
42 | display: none;
43 | }
44 |
45 | .todoapp {
46 | background: #fff;
47 | margin: 130px 0 40px 0;
48 | position: relative;
49 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
50 | 0 25px 50px 0 rgba(0, 0, 0, 0.1);
51 | }
52 |
53 | .todoapp input::-webkit-input-placeholder {
54 | font-style: italic;
55 | font-weight: 300;
56 | color: #e6e6e6;
57 | }
58 |
59 | .todoapp input::-moz-placeholder {
60 | font-style: italic;
61 | font-weight: 300;
62 | color: #e6e6e6;
63 | }
64 |
65 | .todoapp input::input-placeholder {
66 | font-style: italic;
67 | font-weight: 300;
68 | color: #e6e6e6;
69 | }
70 |
71 | .todoapp h1 {
72 | position: absolute;
73 | top: -155px;
74 | width: 100%;
75 | font-size: 100px;
76 | font-weight: 100;
77 | text-align: center;
78 | color: rgba(175, 47, 47, 0.15);
79 | -webkit-text-rendering: optimizeLegibility;
80 | -moz-text-rendering: optimizeLegibility;
81 | text-rendering: optimizeLegibility;
82 | }
83 |
84 | .new-todo,
85 | .edit {
86 | position: relative;
87 | margin: 0;
88 | width: 100%;
89 | font-size: 24px;
90 | font-family: inherit;
91 | font-weight: inherit;
92 | line-height: 1.4em;
93 | border: 0;
94 | outline: none;
95 | color: inherit;
96 | padding: 6px;
97 | border: 1px solid #999;
98 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
99 | box-sizing: border-box;
100 | -webkit-font-smoothing: antialiased;
101 | -moz-osx-font-smoothing: grayscale;
102 | }
103 |
104 | .new-todo {
105 | padding: 16px 16px 16px 60px;
106 | border: none;
107 | background: rgba(0, 0, 0, 0.003);
108 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
109 | }
110 |
111 | .main {
112 | position: relative;
113 | z-index: 2;
114 | border-top: 1px solid #e6e6e6;
115 | }
116 |
117 | label[for='toggle-all'] {
118 | display: none;
119 | }
120 |
121 | .toggle-all {
122 | position: absolute;
123 | top: -55px;
124 | left: -12px;
125 | width: 60px;
126 | height: 34px;
127 | text-align: center;
128 | border: none; /* Mobile Safari */
129 | }
130 |
131 | .toggle-all:before {
132 | content: '❯';
133 | font-size: 22px;
134 | color: #e6e6e6;
135 | padding: 10px 27px 10px 27px;
136 | }
137 |
138 | .toggle-all:checked:before {
139 | color: #737373;
140 | }
141 |
142 | .todo-list {
143 | margin: 0;
144 | padding: 0;
145 | list-style: none;
146 | }
147 |
148 | .todo-list li {
149 | position: relative;
150 | font-size: 24px;
151 | border-bottom: 1px solid #ededed;
152 | }
153 |
154 | .todo-list li:last-child {
155 | border-bottom: none;
156 | }
157 |
158 | .todo-list li.editing {
159 | border-bottom: none;
160 | padding: 0;
161 | }
162 |
163 | .todo-list li.editing .edit {
164 | display: block;
165 | width: 506px;
166 | padding: 13px 17px 12px 17px;
167 | margin: 0 0 0 43px;
168 | }
169 |
170 | .todo-list li.editing .view {
171 | display: none;
172 | }
173 |
174 | .todo-list li .toggle {
175 | text-align: center;
176 | width: 40px;
177 | /* auto, since non-WebKit browsers doesn't support input styling */
178 | height: auto;
179 | position: absolute;
180 | top: 0;
181 | bottom: 0;
182 | margin: auto 0;
183 | border: none; /* Mobile Safari */
184 | -webkit-appearance: none;
185 | appearance: none;
186 | }
187 |
188 | .todo-list li .toggle:after {
189 | content: url('data:image/svg+xml;utf8,');
190 | }
191 |
192 | .todo-list li .toggle:checked:after {
193 | content: url('data:image/svg+xml;utf8,');
194 | }
195 |
196 | .todo-list li label {
197 | white-space: pre-line;
198 | word-break: break-all;
199 | padding: 15px 60px 15px 15px;
200 | margin-left: 45px;
201 | display: block;
202 | line-height: 1.2;
203 | transition: color 0.4s;
204 | }
205 |
206 | .todo-list li.completed label {
207 | color: #d9d9d9;
208 | text-decoration: line-through;
209 | }
210 |
211 | .todo-list li .destroy {
212 | display: none;
213 | position: absolute;
214 | top: 0;
215 | right: 10px;
216 | bottom: 0;
217 | width: 40px;
218 | height: 40px;
219 | margin: auto 0;
220 | font-size: 30px;
221 | color: #cc9a9a;
222 | margin-bottom: 11px;
223 | transition: color 0.2s ease-out;
224 | }
225 |
226 | .todo-list li .destroy:hover {
227 | color: #af5b5e;
228 | }
229 |
230 | .todo-list li .destroy:after {
231 | content: '×';
232 | }
233 |
234 | .todo-list li:hover .destroy {
235 | display: block;
236 | }
237 |
238 | .todo-list li .edit {
239 | display: none;
240 | }
241 |
242 | .todo-list li.editing:last-child {
243 | margin-bottom: -1px;
244 | }
245 |
246 | .footer {
247 | color: #777;
248 | padding: 10px 15px;
249 | height: 20px;
250 | text-align: center;
251 | border-top: 1px solid #e6e6e6;
252 | }
253 |
254 | .footer:before {
255 | content: '';
256 | position: absolute;
257 | right: 0;
258 | bottom: 0;
259 | left: 0;
260 | height: 50px;
261 | overflow: hidden;
262 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
263 | 0 8px 0 -3px #f6f6f6,
264 | 0 9px 1px -3px rgba(0, 0, 0, 0.2),
265 | 0 16px 0 -6px #f6f6f6,
266 | 0 17px 2px -6px rgba(0, 0, 0, 0.2);
267 | }
268 |
269 | .todo-count {
270 | float: left;
271 | text-align: left;
272 | }
273 |
274 | .todo-count strong {
275 | font-weight: 300;
276 | }
277 |
278 | .filters {
279 | margin: 0;
280 | padding: 0;
281 | list-style: none;
282 | position: absolute;
283 | right: 0;
284 | left: 0;
285 | }
286 |
287 | .filters li {
288 | display: inline;
289 | }
290 |
291 | .filters li a {
292 | color: inherit;
293 | margin: 3px;
294 | padding: 3px 7px;
295 | text-decoration: none;
296 | border: 1px solid transparent;
297 | border-radius: 3px;
298 | }
299 |
300 | .filters li a.selected,
301 | .filters li a:hover {
302 | border-color: rgba(175, 47, 47, 0.1);
303 | }
304 |
305 | .filters li a.selected {
306 | border-color: rgba(175, 47, 47, 0.2);
307 | }
308 |
309 | .clear-completed,
310 | html .clear-completed:active {
311 | float: right;
312 | position: relative;
313 | line-height: 20px;
314 | text-decoration: none;
315 | cursor: pointer;
316 | }
317 |
318 | .clear-completed:hover {
319 | text-decoration: underline;
320 | }
321 |
322 | .info {
323 | margin: 65px auto 0;
324 | color: #bfbfbf;
325 | font-size: 10px;
326 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
327 | text-align: center;
328 | }
329 |
330 | .info p {
331 | line-height: 1;
332 | }
333 |
334 | .info a {
335 | color: inherit;
336 | text-decoration: none;
337 | font-weight: 400;
338 | }
339 |
340 | .info a:hover {
341 | text-decoration: underline;
342 | }
343 |
344 | /*
345 | Hack to remove background from Mobile Safari.
346 | Can't use it globally since it destroys checkboxes in Firefox
347 | */
348 | @media screen and (-webkit-min-device-pixel-ratio:0) {
349 | .toggle-all,
350 | .todo-list li .toggle {
351 | background: none;
352 | }
353 |
354 | .todo-list li .toggle {
355 | height: 40px;
356 | }
357 |
358 | .toggle-all {
359 | -webkit-transform: rotate(90deg);
360 | transform: rotate(90deg);
361 | -webkit-appearance: none;
362 | appearance: none;
363 | }
364 | }
365 |
366 | @media (max-width: 430px) {
367 | .footer {
368 | height: 50px;
369 | }
370 |
371 | .filters {
372 | bottom: 10px;
373 | }
374 | }
375 |
376 | .locale-selector {
377 | position: fixed;
378 | top: 10px;
379 | left: 10px;
380 | }
381 |
382 | .support-request {
383 | position: fixed;
384 | top: 10px;
385 | right: 10px;
386 | }
387 |
--------------------------------------------------------------------------------
/src/test/com/fulcrologic/fulcro/macros/defsc_spec.clj:
--------------------------------------------------------------------------------
1 | (ns com.fulcrologic.fulcro.macros.defsc-spec
2 | (:require
3 | [com.fulcrologic.fulcro.components :as defsc]
4 | [fulcro-spec.core :refer [assertions specification component]]
5 | [clojure.test :refer [deftest is]]
6 | [com.fulcrologic.fulcro.algorithms.do-not-use :as util])
7 | (:import (clojure.lang ExceptionInfo)))
8 |
9 | (declare =>)
10 |
11 | (deftest ^:focus destructured-keys-test
12 | (assertions
13 | "Finds the correct keys for arbitrary destructuring"
14 | (util/destructured-keys '{:keys [a b]}) => #{:a :b}
15 | (util/destructured-keys '{:some-ns/keys [a b]}) => #{:some-ns/a :some-ns/b}
16 | (util/destructured-keys '{:x/keys [a b]
17 | :y/keys [n m]}) => #{:x/a :x/b :y/n :y/m}
18 | (util/destructured-keys '{:y/keys [n m]
19 | boo :gobble/that}) => #{:y/n :y/m :gobble/that}
20 | (util/destructured-keys '{:keys [:a.b/n db/id]
21 | ::keys [x]
22 | }) => #{:a.b/n :db/id ::x}))
23 |
24 | (deftest defsc-macro-helpers-test
25 | (component "build-render form"
26 | (assertions
27 | "emits a list of forms for the render itself"
28 | (#'defsc/build-render 'Boo 'this {:keys ['a]} {:keys ['onSelect]} nil '((dom/div nil "Hello")))
29 | => `(~'fn ~'render-Boo [~'this]
30 | (com.fulcrologic.fulcro.components/wrapped-render ~'this
31 | (fn []
32 | (let [{:keys [~'a]} (com.fulcrologic.fulcro.components/props ~'this)
33 | {:keys [~'onSelect]} (com.fulcrologic.fulcro.components/get-computed ~'this)]
34 | (~'dom/div nil "Hello")))))
35 | "all arguments after props are optional"
36 | (#'defsc/build-render 'Boo 'this {:keys ['a]} nil nil '((dom/div nil "Hello")))
37 | => `(~'fn ~'render-Boo [~'this]
38 | (com.fulcrologic.fulcro.components/wrapped-render ~'this
39 | (fn []
40 | (let [{:keys [~'a]} (com.fulcrologic.fulcro.components/props ~'this)]
41 | (~'dom/div nil "Hello")))))
42 | "destructuring of css is allowed"
43 | (#'defsc/build-render 'Boo 'this {:keys ['a]} {:keys ['onSelect]} '{:keys [my-class]} '((dom/div nil "Hello")))
44 | => `(~'fn ~'render-Boo [~'this]
45 | (com.fulcrologic.fulcro.components/wrapped-render ~'this
46 | (fn []
47 | (let [{:keys [~'a]} (com.fulcrologic.fulcro.components/props ~'this)
48 | {:keys [~'onSelect]} (com.fulcrologic.fulcro.components/get-computed ~'this)
49 | ~'{:keys [my-class]} (com.fulcrologic.fulcro.components/get-extra-props ~'this)]
50 | (~'dom/div nil "Hello")))))))
51 | (component "build-query-forms"
52 | (is (thrown-with-msg? ExceptionInfo #"defsc X: .person/nme.* was destructured"
53 | (#'defsc/build-query-forms nil 'X 'this '{:keys [db/id person/nme person/job]}
54 | {:template '[:db/id :person/name {:person/job (defsc/get-query Job)}]}))
55 | "Verifies the propargs matches queries data when not a symbol")
56 | (assertions
57 | "Support a method form"
58 | (#'defsc/build-query-forms nil 'X 'this 'props {:method '(fn [] [:db/id])})
59 | => '(fn query* [this] [:db/id])
60 | "Uses symbol from external-looking scope in output"
61 | (#'defsc/build-query-forms nil 'X 'that 'props {:method '(query [] [:db/id])})
62 | => '(fn query* [that] [:db/id])
63 | "Honors the symbol for this that is defined by defsc"
64 | (#'defsc/build-query-forms nil 'X 'that 'props {:template '[:db/id]})
65 | => '(fn query* [that] [:db/id])
66 | "Composes properties and joins into a proper query expression as a list of defui forms"
67 | (#'defsc/build-query-forms nil 'X 'this 'props {:template '[:db/id :person/name {:person/job (defsc/get-query Job)} {:person/settings (defsc/get-query Settings)}]})
68 | => `(~'fn ~'query* [~'this] [:db/id :person/name {:person/job (~'defsc/get-query ~'Job)} {:person/settings (~'defsc/get-query ~'Settings)}])))
69 | (component "build-ident form"
70 | (is (thrown-with-msg? ExceptionInfo #"The ID property " (#'defsc/build-ident nil 't 'p {:template [:TABLE/by-id :id]} #{})) "Throws if the query/ident template don't match")
71 | (is (thrown-with-msg? ExceptionInfo #"The table/id :id of " (#'defsc/build-ident nil 't 'p {:keyword :id} #{})) "Throws if the keyword isn't found in the query")
72 | (assertions
73 | "Generates nothing when there is no table"
74 | (#'defsc/build-ident nil 't 'p nil #{}) => nil
75 | (#'defsc/build-ident nil 't 'p nil #{:boo}) => nil
76 | "Can use a simple keyword for both table and id"
77 | (#'defsc/build-ident nil 't '{:keys [a b c]} {:keyword :person/id} #{:person/id})
78 | => '(fn ident* [_ props] [:person/id (:person/id props)])
79 | "Can use a ident method to build the defui forms"
80 | (#'defsc/build-ident nil 't 'p {:method '(fn [] [:x :id])} #{})
81 | => '(fn ident* [t p] [:x :id])
82 | "Can include destructuring in props"
83 | (#'defsc/build-ident nil 't '{:keys [a b c]} {:method '(fn [] [:x :id])} #{})
84 | => '(fn ident* [t {:keys [a b c]}] [:x :id])
85 | "Can use a vector template to generate defui forms"
86 | (#'defsc/build-ident nil 't 'p {:template [:TABLE/by-id :id]} #{:id})
87 | => '(fn ident* [this props] [:TABLE/by-id (:id props)])))
88 | (component "build-initial-state"
89 | (is (thrown-with-msg? ExceptionInfo #"defsc S: Illegal parameters to :initial-state"
90 | (#'defsc/build-initial-state nil 'S {:template {:child '(get-initial-state P {})}} #{:child}
91 | '{:template [{:child (defsc/get-query S)}]})) "Throws an error in template mode if any of the values are calls to get-initial-state")
92 | (is (thrown-with-msg? ExceptionInfo #"When query is a method, initial state MUST"
93 | (#'defsc/build-initial-state nil 'S {:template {:x 1}} #{} {:method '(fn [t] [])}))
94 | "If the query is a method, the initial state must be as well")
95 | (is (thrown-with-msg? ExceptionInfo #"Initial state includes keys"
96 | (#'defsc/build-initial-state nil 'S {:template {:x 1}} #{} {:template [:x]}))
97 | "In template mode: Disallows initial state to contain items that are not in the query")
98 | (assertions
99 | "Generates nothing when there is entry"
100 | (#'defsc/build-initial-state nil 'S nil #{} {:template []}) => nil
101 | "Can build initial state from a method"
102 | (#'defsc/build-initial-state nil 'S {:method '(fn [p] {:x 1})} #{} {:template []}) =>
103 | '(fn build-raw-initial-state* [p] {:x 1})
104 | "Can build initial state from a template"
105 | (#'defsc/build-initial-state nil 'S {:template {}} #{} {:template []}) =>
106 | '(fn build-initial-state* [params] (com.fulcrologic.fulcro.components/make-state-map {} {} params))
107 | "Allows any state in initial-state method form, independent of the query form"
108 | (#'defsc/build-initial-state nil 'S {:method '(fn [p] {:x 1 :y 2})} #{} {:tempate []})
109 | => '(fn build-raw-initial-state* [p] {:x 1 :y 2})
110 | (#'defsc/build-initial-state nil 'S {:method '(initial-state [p] {:x 1 :y 2})} #{} {:method '(query [t] [])})
111 | => '(fn build-raw-initial-state* [p] {:x 1 :y 2})
112 | "Generates proper state parameters to make-state-map when data is available"
113 | (#'defsc/build-initial-state nil 'S {:template {:x 1}} #{:x} {:template [:x]})
114 | => '(fn build-initial-state* [params]
115 | (com.fulcrologic.fulcro.components/make-state-map {:x 1} {} params))))
116 | (component "replace-and-validate-fn"
117 | (is (thrown-with-msg? ExceptionInfo #"Invalid arity for nm"
118 | (#'defsc/replace-and-validate-fn nil 'nm ['a] 2 '(fn [p] ...)))
119 | "Throws an exception if there are too few arguments")
120 | (assertions
121 | "Replaces the first symbol in a method/lambda form"
122 | (#'defsc/replace-and-validate-fn nil 'nm [] 0 '(fn [] ...)) => '(fn nm [] ...)
123 | "Allows correct number of args"
124 | (#'defsc/replace-and-validate-fn nil 'nm ['this] 1 '(fn [x] ...)) => '(fn nm [this x] ...)
125 | "Allows too many args"
126 | (#'defsc/replace-and-validate-fn nil 'nm ['this] 1 '(fn [x y z] ...)) => '(fn nm [this x y z] ...)
127 | "Prepends the additional arguments to the front of the argument list"
128 | (#'defsc/replace-and-validate-fn nil 'nm ['that 'other-thing] 3 '(fn [x y z] ...)) => '(fn nm [that other-thing x y z] ...))))
129 |
130 |
--------------------------------------------------------------------------------
/src/test/com/fulcrologic/fulcro/server/config_spec.clj:
--------------------------------------------------------------------------------
1 | (ns com.fulcrologic.fulcro.server.config-spec
2 | (:require
3 | [clojure.test :as t :refer [are is]]
4 | [fulcro-spec.core :refer [specification assertions provided component behavior when-mocking]]
5 | [com.fulcrologic.fulcro.server.config :as server])
6 | (:import (java.io File)))
7 |
8 | (declare =>)
9 |
10 | (def dflt-cfg {:port 1337})
11 |
12 | (defn with-tmp-edn-file
13 | "Creates a temporary edn file with stringified `contents`,
14 | calls `f` with its absolute path,
15 | and returns the result after deleting the file."
16 | [contents f]
17 | (let [tmp-file (File/createTempFile "tmp-file" ".edn")
18 | _ (spit tmp-file (str contents))
19 | abs-path (.getAbsolutePath tmp-file)
20 | res (f abs-path)]
21 | (.delete tmp-file) res))
22 |
23 | (specification "load-config"
24 | (behavior "If the user specifies an empty default path"
25 | (when-mocking
26 | (server/load-edn-file! d) =1x=> (do
27 | (assertions
28 | "it uses the normal default"
29 | d => "config/defaults.edn")
30 | {})
31 | (server/load-edn-file! f) =1x=> {:k :v}
32 |
33 | (server/load-config! {:defaults-path ""})))
34 |
35 | (behavior "If the user specifies an nil default path"
36 | (when-mocking
37 | (server/load-edn-file! d) =1x=> (do
38 | (assertions
39 | "it uses the normal default"
40 | d => "config/defaults.edn")
41 | {})
42 | (server/load-edn-file! f) =1x=> {:k :v}
43 |
44 | (server/load-config! {:defaults-path nil})))
45 |
46 | (behavior "If a user specifies and alternate defaults:"
47 | (when-mocking
48 | (server/load-edn-file! d) =1x=> (do
49 | (assertions
50 | "Alternate is used for the defaults"
51 | d => "override.edn")
52 | {})
53 | (server/load-edn-file! f) =1x=> {:k :v}
54 |
55 | (server/load-config! {:defaults-path "override.edn"})))
56 |
57 | (when-mocking
58 | (server/load-edn-file! d) =1x=> (do
59 | (assertions
60 | "Looks for the defaults file"
61 | d => "config/defaults.edn")
62 | {})
63 | (server/get-system-prop prop) => (do
64 | (assertions
65 | "uses the system property config"
66 | prop => "config")
67 | "some-file")
68 | (server/load-edn-file! f) =1x=> (do
69 | (assertions
70 | "to find the supplied config file"
71 | f => "some-file")
72 | {:k :v})
73 |
74 | (assertions
75 | "looks for system property -Dconfig"
76 | (server/load-config! {}) => {:k :v}))
77 |
78 | (behavior "does not fail when returning nil"
79 | (assertions
80 | (server/get-system-prop "config") => nil))
81 | (behavior "defaults file is always used to provide missing values"
82 | (when-mocking
83 | (server/load-edn-file! defaults-path) =1x=> {:a :b}
84 | (server/load-edn-file! nil) =1x=> {:c :d}
85 | (assertions
86 | (server/load-config! {}) => {:a :b
87 | :c :d})))
88 |
89 | (behavior "config file overrides defaults"
90 | (when-mocking
91 | (server/load-edn-file! defaults-path) =1x=> {:a {:b {:c :d}
92 | :e {:z :v}}}
93 | (server/load-edn-file! nil) =1x=> {:a {:b {:c :f
94 | :u :y}
95 | :e 13}}
96 | (assertions (server/load-config! {}) => {:a {:b {:c :f
97 | :u :y}
98 | :e 13}})))
99 |
100 | (component "load-config"
101 | (behavior "crashes if no default is found"
102 | (assertions
103 | (server/load-config! {}) =throws=> #"Invalid config"))
104 | (behavior "crashes if no config is found"
105 | (assertions (server/load-config! {:config-path "/tmp/asdlfkjhadsflkjh"}) =throws=> #"Invalid config"))
106 | (behavior "falls back to `config-path`"
107 | (when-mocking
108 | (server/load-edn-file! defaults-path) =1x=> {}
109 | (server/load-edn-file! "/some/path") =1x=> {:k :v}
110 | (assertions (server/load-config! {:config-path "/some/path"}) => {:k :v})))
111 | (behavior "recursively resolves symbols using resolve-symbol"
112 | (when-mocking
113 | (server/load-edn-file! defaults-path) =1x=> {:a {:b {:c 'clojure.core/symbol}}
114 | :v [0 "d"]
115 | :s #{'clojure.core/symbol}}
116 | (server/load-edn-file! nil) =1x=> {}
117 | (assertions (server/load-config! {}) => {:a {:b {:c #'clojure.core/symbol}}
118 | :v [0 "d"]
119 | :s #{#'clojure.core/symbol}})))
120 | (behavior "passes config-path to get-config"
121 | (when-mocking
122 | (server/load-edn-file! defaults-path) =1x=> {}
123 | (server/load-edn-file! "/foo/bar") =1x=> {}
124 | (assertions (server/load-config! {:config-path "/foo/bar"}) => {})))
125 | (assertions
126 | "config-path can be a relative path"
127 | (server/load-config! {:config-path "not/abs/path"})
128 | =throws=> #"Invalid config file"
129 |
130 | "prints the invalid path in the exception message"
131 | (server/load-config! {:config-path "invalid/file"})
132 | =throws=> #"invalid/file"))
133 |
134 | (component "load-edn"
135 | (behavior "returns nil if absolute file is not found"
136 | (assertions (#'server/load-edn! "/garbage") => nil))
137 | (behavior "returns nil if relative file is not on classpath"
138 | (assertions (#'server/load-edn! "garbage") => nil))
139 | (behavior "can load edn from the classpath"
140 | (assertions (:some-key (#'server/load-edn! "config/other.edn")) => :some-default-val))
141 | (behavior "can load edn with :env/vars"
142 | (when-mocking
143 | (server/get-system-env "FAKE_ENV_VAR") => "FAKE STUFF"
144 | (server/load-edn-file! defaults-path) =1x=> {}
145 | (server/get-system-prop "config") => :..cfg-path..
146 | (server/load-edn-file! :..cfg-path..) =1x=> {:fake :env/FAKE_ENV_VAR}
147 | (assertions (server/load-config!) => {:fake "FAKE STUFF"}))
148 | (behavior "when the namespace is env.edn it will edn/read-string it"
149 | (when-mocking
150 | (server/get-system-env "FAKE_ENV_VAR") => "3000"
151 | (server/load-edn-file! defaults-path) =1x=> {}
152 | (server/get-system-prop "config") => :..cfg-path..
153 | (server/load-edn-file! :..cfg-path..) =1x=> {:fake :env.edn/FAKE_ENV_VAR}
154 | (assertions (server/load-config!) => {:fake 3000}))
155 | (behavior "buyer beware as it'll parse it in ways you might not expect!"
156 | (when-mocking
157 | (server/get-system-env "FAKE_ENV_VAR") => "some-symbol"
158 | (server/load-edn-file! defaults-path) =1x=> {}
159 | (server/get-system-prop "config") => :..cfg-path..
160 | (server/load-edn-file! :..cfg-path..) =1x=> {:fake :env.edn/FAKE_ENV_VAR}
161 | (assertions (server/load-config!) => {:fake 'some-symbol}))))))
162 | (component "open-config-file"
163 | (behavior "takes in a path, finds the file at that path and should return a clojure map"
164 | (when-mocking
165 | (server/load-edn! "/foobar") => "42"
166 | (assertions
167 | (#'server/load-edn-file! "/foobar") => "42")))
168 | (behavior "or if path is nil, uses a default path"
169 | (assertions
170 | (#'server/load-edn-file! nil) =throws=> #"Invalid config file"))
171 | (behavior "if path doesn't exist on fs, it throws an ex-info"
172 | (assertions
173 | (#'server/load-edn-file! "/should/fail") =throws=> #"Invalid config file"))))
174 |
175 |
176 |
--------------------------------------------------------------------------------
/src/main/com/fulcrologic/fulcro/server/api_middleware.clj:
--------------------------------------------------------------------------------
1 | (ns com.fulcrologic.fulcro.server.api-middleware
2 | (:require
3 | [clojure.pprint :refer [pprint]]
4 | [clojure.test :refer :all]
5 | [clojure.repl :refer [doc source]]
6 | [com.fulcrologic.fulcro.algorithms.transit :as transit]
7 | [cognitect.transit :as ct]
8 | [taoensso.timbre :as log])
9 | (:import (java.io ByteArrayOutputStream)))
10 |
11 | (def not-found-handler
12 | (fn [req]
13 | {:status 404
14 | :headers {"Content-Type" "text/plain"}
15 | :body "Invalid request."}))
16 |
17 | (defn generate-response
18 | "Generate a Fulcro-compatible response containing at least a status code, headers, and body. You should
19 | pre-populate at least the body of the input-response.
20 | The content type of the returned response will always be pegged to 'application/transit+json'."
21 | [{:keys [status body headers] :or {status 200} :as input-response}]
22 | (-> (assoc input-response :status status :body body)
23 | (update :headers assoc "Content-Type" "application/transit+json")))
24 |
25 | (defn augment-response
26 | "Adds a lambda to the given data `core-response` such that `apply-response-augmentations`
27 | will use it to morph the raw Ring response in which the `core-response` is embedded
28 | (the core response becomes the `:body`).
29 |
30 | The `ring-response-fn` is a `(fn [resp] resp')` that will be passed a raw (possibly empty)
31 | Ring response which it can modify and return.
32 |
33 | Use this function when you need to add information into the handler response, for
34 | example when you need to add cookies or session data. Example:
35 |
36 | (defmutation my-mutate
37 | ...
38 | (augment-response
39 | {:uid 42} ; your regular response
40 | #(assoc-in % [:session :user-id] 42))) ; a function resp -> resp
41 |
42 | If the parser has multiple responses that use `augment-response` they will all be applied.
43 | The first one will receive an empty map as input. Only top level values
44 | of your response will be checked for augmented response (i.e. primarily mutation responses).
45 |
46 | See `apply-response-augmentations`, which is used by `handle-api-request`, which in turn is the
47 | primary implementation element of `wrap-api`."
48 | [core-response ring-response-fn]
49 | (assert (instance? clojure.lang.IObj core-response) "Scalar values can't be augmented.")
50 | (with-meta core-response {::augment-response ring-response-fn}))
51 |
52 | (defn apply-response-augmentations
53 | "Process the raw response from the parser looking for lambdas that were added by
54 | top-level Fulcro queries and mutations via
55 | `augment-response`. Runs each in turn and accumulates their effects. The result is
56 | meant to be a Ring response (and is used as such by `handle-api-request`."
57 | [response]
58 | (->> (keep #(some-> (second %) meta ::augment-response) response)
59 | (reduce (fn [response f] (f response)) {})))
60 |
61 | (defn handle-api-request
62 | "Given a parser and a query: Runs the parser on the query,
63 | and generates a standard Fulcro-compatible response, and augment the raw Ring response with
64 | any augment handlers that were indicated on top-level mutations/queries via
65 | `augment-response`."
66 | [query query-processor]
67 | (generate-response
68 | (let [parse-result (try
69 | (query-processor query)
70 | (catch Exception e
71 | (log/error e "Parser threw an exception on" query)
72 | e))]
73 | (if (instance? Throwable parse-result)
74 | {:status 500 :body "Internal server error. Parser threw an exception. See server logs for details."}
75 | (merge {:status 200 :body parse-result} (apply-response-augmentations parse-result))))))
76 |
77 | (defn reader
78 | "Create a transit reader. This reader can handler the tempid type.
79 | Can pass transit reader customization opts map."
80 | ([in] (transit/reader in))
81 | ([in opts] (transit/reader in opts)))
82 |
83 | (defn writer
84 | "Create a transit writer. This writer can handler the tempid type.
85 | Can pass transit writer customization opts map."
86 | ([out] (transit/writer out))
87 | ([out opts] (transit/writer out opts)))
88 |
89 | (defn- get-content-type
90 | "Return the content-type of the request, or nil if no content-type is set. Defined here to limit the need for Ring."
91 | [request]
92 | (if-let [type (get-in request [:headers "content-type"])]
93 | (second (re-find #"^(.*?)(?:;|$)" type))))
94 |
95 | (defn- transit-request? [request]
96 | (if-let [type (get-content-type request)]
97 | (let [mtch (re-find #"^application/transit\+(json|msgpack)" type)]
98 | [(not (empty? mtch)) (keyword (second mtch))])))
99 |
100 | (defn- read-transit [request {:keys [opts]}]
101 | (let [[res _] (transit-request? request)]
102 | (if res
103 | (if-let [body (:body request)]
104 | (let [rdr (reader body opts)]
105 | (try
106 | [true (ct/read rdr)]
107 | (catch Exception ex
108 | [false nil])))))))
109 |
110 | (def ^{:doc "The default response to return when a Transit request is malformed."}
111 | default-malformed-response
112 | {:status 400
113 | :headers {"Content-Type" "text/plain"}
114 | :body "Malformed Transit in request body."})
115 |
116 | (defn- assoc-transit-params [request transit]
117 | (let [request (assoc request :transit-params transit)]
118 | (if (map? transit)
119 | (update-in request [:params] merge transit)
120 | request)))
121 |
122 | (defn wrap-transit-params
123 | "Middleware that parses the body of Transit requests into a map of parameters,
124 | which are added to the request map on the :transit-params and :params keys.
125 | Accepts the following options:
126 | :malformed-response - a response map to return when the JSON is malformed
127 | :opts - a map of options to be passed to the transit reader
128 | Use the standard Ring middleware, ring.middleware.keyword-params, to
129 | convert the parameters into keywords."
130 | {:arglists '([handler] [handler options])}
131 | [handler & [{:keys [malformed-response]
132 | :or {malformed-response default-malformed-response}
133 | :as options}]]
134 | (fn [request]
135 | (if-let [[valid? transit] (read-transit request options)]
136 | (if valid?
137 | (handler (assoc-transit-params request transit))
138 | malformed-response)
139 | (handler request))))
140 |
141 | (defn- set-content-type
142 | "Returns an updated Ring response with the a Content-Type header corresponding
143 | to the given content-type. This is defined here so non-ring users do not need ring."
144 | [resp content-type]
145 | (assoc-in resp [:headers "Content-Type"] (str content-type)))
146 |
147 | (defn- write [x t opts]
148 | (let [baos (ByteArrayOutputStream.)
149 | w (writer baos opts)
150 | _ (ct/write w x)
151 | ret (.toString baos)]
152 | (.reset baos)
153 | ret))
154 |
155 | (defn wrap-transit-response
156 | "Middleware that converts responses with a map or a vector for a body into a
157 | Transit response.
158 | Accepts the following options:
159 | :encoding - one of #{:json :json-verbose :msgpack}
160 | :opts - a map of options to be passed to the transit writer"
161 | {:arglists '([handler] [handler options])}
162 | [handler & [{:as options}]]
163 | (let [{:keys [encoding opts] :or {encoding :json}} options]
164 | (assert (#{:json :json-verbose :msgpack} encoding) "The encoding must be one of #{:json :json-verbose :msgpack}.")
165 | (fn [request]
166 | (let [response (handler request)]
167 | (if (coll? (:body response))
168 | (let [transit-response (update-in response [:body] write encoding opts)]
169 | (if (contains? (:headers response) "Content-Type")
170 | transit-response
171 | (set-content-type transit-response (format "application/transit+%s; charset=utf-8" (name encoding)))))
172 | response)))))
173 |
174 | (defn wrap-api
175 | "Wrap Fulcro API request processing. Required options are:
176 |
177 | - `:uri` - The URI on the server that handles the API requests.
178 | - `:parser` - A function `(fn [eql-query] eql-response)` that can process the query.
179 |
180 | IMPORTANT: You must install `wrap-transit-response` and `wrap-transit-params` to your middleware below this."
181 | [handler {:keys [uri parser]}]
182 | (when-not (and (string? uri) (fn? parser))
183 | (throw (ex-info "Invalid parameters to `wrap-api`. :uri and :parser are required. See docstring." {})))
184 | (fn [request]
185 | ;; eliminates overhead of wrap-transit
186 | (if (= uri (:uri request))
187 | (handle-api-request (:transit-params request) parser)
188 | (handler request))))
189 |
--------------------------------------------------------------------------------
/src/main/com/fulcrologic/fulcro/algorithms/data_targeting.cljc:
--------------------------------------------------------------------------------
1 | (ns com.fulcrologic.fulcro.algorithms.data-targeting
2 | "The implementation of processing load/mutation result graph targeting."
3 | (:require
4 | [clojure.spec.alpha :as s]
5 | [clojure.set :as set]
6 | [ghostwheel.core :as gw :refer [>defn =>]]
7 | [taoensso.timbre :as log]
8 | [edn-query-language.core :as eql]))
9 |
10 | (s/def ::target vector?)
11 |
12 | (>defn multiple-targets
13 | "Specifies a target that should place edges in the graph at multiple locations.
14 |
15 | `targets` - Any number of targets. A target can be a simple path (as a vector), or other
16 | special targets like `append-to` and `prepend-to`."
17 | [& targets]
18 | [(s/* ::target) => ::target]
19 | (with-meta (vec targets) {::multiple-targets true}))
20 |
21 | (>defn prepend-to
22 | "Specifies a to-many target that will preprend an edge to some to-many edge. NOTE: this kind of target will not
23 | create duplicates in the target list.
24 |
25 | `target` - A vector (path) in the normalized database of the to-many list of idents.
26 | "
27 | [target]
28 | [::target => ::target]
29 | (with-meta target {::prepend-target true}))
30 |
31 | (>defn append-to
32 | "Specifies a to-many target that will append an edge to some to-many edge. NOTE: this kind of target will not
33 | create duplicates in the target list.
34 |
35 | `target` - A vector (path) in the normalized database of the to-many list of idents."
36 | [target]
37 | [::target => ::target]
38 | (with-meta target {::append-target true}))
39 |
40 | (>defn replace-at
41 | "Specifies a target that will replace an edge at some normalized location.
42 |
43 | `target` - A vector (path) in the normalized database. This path can include numbers to target some element
44 | of an existing to-many list of idents."
45 | [target]
46 | [::target => ::target]
47 | (with-meta target {::replace-target true}))
48 |
49 | (>defn replacement-target? [t] [any? => boolean?] (-> t meta ::replace-target boolean))
50 | (>defn prepend-target? [t] [any? => boolean?] (-> t meta ::prepend-target boolean))
51 | (>defn append-target? [t] [any? => boolean?] (-> t meta ::append-target boolean))
52 | (>defn multiple-targets? [t] [any? => boolean?] (-> t meta ::multiple-targets boolean))
53 |
54 | (>defn special-target?
55 | "Is the given target special? This means it is not just a plain vector path, but is instead something like
56 | an append."
57 | [target]
58 | [any? => boolean?]
59 | (boolean (seq (set/intersection (-> target meta keys set) #{::replace-target ::append-target ::prepend-target ::multiple-targets}))))
60 |
61 | (>defn integrate-ident*
62 | "Integrate an ident into any number of places in the app state. This function is safe to use within mutation
63 | implementations as a general helper function.
64 |
65 | The named parameters can be specified any number of times. They are:
66 |
67 | - append: A vector (path) to a list in your app state where this new object's ident should be appended. Will not append
68 | the ident if that ident is already in the list.
69 | - prepend: A vector (path) to a list in your app state where this new object's ident should be prepended. Will not place
70 | the ident if that ident is already in the list.
71 | - replace: A vector (path) to a specific location in app-state where this object's ident should be placed. Can target a to-one or to-many.
72 | If the target is a vector element then that element must already exist in the vector.
73 |
74 | NOTE: `ident` does not have to be an ident if you want to place denormalized data. It can really be anything.
75 |
76 | Returns the updated state map."
77 | [state ident & named-parameters]
78 | [map? any? (s/* (s/or :path ::target :command #{:append :prepend :replace})) => map?]
79 | (let [actions (partition 2 named-parameters)]
80 | (reduce (fn [state [command data-path]]
81 | (let [already-has-ident-at-path? (fn [data-path] (some #(= % ident) (get-in state data-path)))]
82 | (case command
83 | :prepend (if (already-has-ident-at-path? data-path)
84 | state
85 | (update-in state data-path #(into [ident] %)))
86 | :append (if (already-has-ident-at-path? data-path)
87 | state
88 | (update-in state data-path (fnil conj []) ident))
89 | :replace (let [path-to-vector (butlast data-path)
90 | to-many? (and (seq path-to-vector) (vector? (get-in state path-to-vector)))
91 | index (last data-path)
92 | vector (get-in state path-to-vector)]
93 | (when-not (vector? data-path) (log/error "Replacement path must be a vector. You passed: " data-path))
94 | (when to-many?
95 | (cond
96 | (not (vector? vector)) (log/error "Path for replacement must be a vector")
97 | (not (number? index)) (log/error "Path for replacement must end in a vector index")
98 | (not (contains? vector index)) (log/error "Target vector for replacement does not have an item at index " index)))
99 | (assoc-in state data-path ident))
100 | state)))
101 | state actions)))
102 |
103 | (>defn process-target
104 | "Process a load target (which can be a multiple-target).
105 |
106 | `state-map` - the state-map
107 | `source-path` - A keyword, ident, or app-state path. If the source path is an ident, then that is what is placed
108 | in app state. If it is a keyword or longer path then the thing at that location in app state is pulled from app state
109 | and copied to the target location(s).
110 | `target` - The target(s)
111 | `remove-source?` - When true the source will be removed from app state once it has been written to the new location.
112 |
113 | Returns an updated state-map with the given changes."
114 | ([state-map source-path target]
115 | [map? (s/or :key keyword? :ident eql/ident? :path vector?) ::target => map?]
116 | (process-target state-map source-path target true))
117 | ([state-map source-path target remove-source?]
118 | [map? (s/or :key keyword? :ident eql/ident? :path vector?) ::target boolean? => map?]
119 | (let [item-to-place (cond (eql/ident? source-path) source-path
120 | (keyword? source-path) (get state-map source-path)
121 | :else (get-in state-map source-path))
122 | many-idents? (and (vector? item-to-place)
123 | (every? eql/ident? item-to-place))]
124 | (cond
125 | (and (eql/ident? source-path)
126 | (not (special-target? target))) (-> state-map
127 | (assoc-in target item-to-place))
128 | (not (special-target? target)) (cond->
129 | (assoc-in state-map target item-to-place)
130 | remove-source? (dissoc source-path))
131 | (multiple-targets? target) (cond-> (reduce (fn [s t] (process-target s source-path t false)) state-map target)
132 | (and (not (eql/ident? source-path)) remove-source?) (dissoc source-path))
133 | (and many-idents? (special-target? target)) (let [state (if remove-source?
134 | (dissoc state-map source-path)
135 | state-map)
136 | target-has-many? (vector? (get-in state target))]
137 | (if target-has-many?
138 | (cond
139 | (prepend-target? target) (update-in state target (fn [v] (vec (concat item-to-place v))))
140 | (append-target? target) (update-in state target (fn [v] (vec (concat v item-to-place))))
141 | :else state)
142 | (assoc-in state target item-to-place)))
143 | (special-target? target) (cond-> state-map
144 | remove-source? (dissoc source-path)
145 | (prepend-target? target) (integrate-ident* item-to-place :prepend target)
146 | (append-target? target) (integrate-ident* item-to-place :append target)
147 | (replacement-target? target) (integrate-ident* item-to-place :replace target))
148 | :else state-map))))
149 |
--------------------------------------------------------------------------------
/src/todomvc/fulcro_todomvc/ui_with_legacy_ui_routers.cljs:
--------------------------------------------------------------------------------
1 | (ns fulcro-todomvc.ui-with-legacy-ui-routers
2 | (:require
3 | [com.fulcrologic.fulcro.algorithms.tempid :as tmp]
4 | [com.fulcrologic.fulcro.application :as app]
5 | [com.fulcrologic.fulcro.components :as comp :refer [defsc]]
6 | [com.fulcrologic.fulcro.dom :as dom]
7 | [com.fulcrologic.fulcro.mutations :as mut :refer [defmutation]]
8 | [com.fulcrologic.fulcro.routing.legacy-ui-routers :as fr]
9 | [fulcro-todomvc.api :as api]
10 | [goog.object :as gobj]
11 | [taoensso.timbre :as log]))
12 |
13 | (defn is-enter? [evt] (= 13 (.-keyCode evt)))
14 | (defn is-escape? [evt] (= 27 (.-keyCode evt)))
15 |
16 | (defn trim-text [text]
17 | "Returns text without surrounding whitespace if not empty, otherwise nil"
18 | (let [trimmed-text (clojure.string/trim text)]
19 | (when-not (empty? trimmed-text)
20 | trimmed-text)))
21 |
22 | (defsc TodoItem [this
23 | {:ui/keys [ui/editing ui/edit-text]
24 | :item/keys [id label complete] :or {complete false} :as props}
25 | {:keys [delete-item check uncheck] :as computed}]
26 | {:query (fn [] [:item/id :item/label :item/complete :ui/editing :ui/edit-text])
27 | :ident :item/id
28 | :initLocalState (fn [this] {:save-ref (fn [r] (gobj/set this "input-ref" r))})
29 | :componentDidUpdate (fn [this prev-props _]
30 | ;; Code adapted from React TodoMVC implementation
31 | (when (and (not (:editing prev-props)) (:editing (comp/props this)))
32 | (let [input-field (gobj/get this "input-ref")
33 | input-field-length (when input-field (.. input-field -value -length))]
34 | (when input-field
35 | (.focus input-field)
36 | (.setSelectionRange input-field input-field-length input-field-length)))))}
37 | (let [submit-edit (fn [evt]
38 | (if-let [trimmed-text (trim-text (.. evt -target -value))]
39 | (do
40 | (comp/transact! this [(api/commit-label-change {:id id :text trimmed-text})])
41 | (mut/set-string! this :ui/edit-text :value trimmed-text)
42 | (mut/toggle! this :ui/editing))
43 | (delete-item id)))]
44 |
45 | (dom/li {:classes [(when complete (str "completed")) (when editing (str " editing"))]}
46 | (dom/div :.view {}
47 | (dom/input {:type "checkbox"
48 | :ref (comp/get-state this :save-ref)
49 | :className "toggle"
50 | :checked (boolean complete)
51 | :onChange #(if complete (uncheck id) (check id))})
52 | (dom/label {:onDoubleClick (fn []
53 | (mut/toggle! this :ui/editing)
54 | (mut/set-string! this :ui/edit-text :value label))} label)
55 | (dom/button :.destroy {:onClick #(delete-item id)}))
56 | (dom/input {:ref "edit_field"
57 | :className "edit"
58 | :value (or edit-text "")
59 | :onChange #(mut/set-string! this :ui/edit-text :event %)
60 | :onKeyDown #(cond
61 | (is-enter? %) (submit-edit %)
62 | (is-escape? %) (do (mut/set-string! this :ui/edit-text :value label)
63 | (mut/toggle! this :ui/editing)))
64 | :onBlur #(when editing (submit-edit %))}))))
65 |
66 | (def ui-todo-item (comp/factory TodoItem {:keyfn :item/id}))
67 |
68 | (defn header [component title]
69 | (let [{:keys [list/id ui/new-item-text]} (comp/props component)]
70 | (dom/header :.header {}
71 | (dom/h1 {} title)
72 | (dom/input {:value (or new-item-text "")
73 | :className "new-todo"
74 | :onKeyDown (fn [evt]
75 | (when (is-enter? evt)
76 | (when-let [trimmed-text (trim-text (.. evt -target -value))]
77 | (comp/transact! component `[(api/todo-new-item ~{:list-id id
78 | :id (tmp/tempid)
79 | :text trimmed-text})]))))
80 | :onChange (fn [evt] (mut/set-string! component :ui/new-item-text :event evt))
81 | :placeholder "What needs to be done?"
82 | :autoFocus true}))))
83 |
84 | (defn filter-footer [component num-todos num-completed]
85 | (let [{:keys [list/id list/filter]} (comp/props component)
86 | num-remaining (- num-todos num-completed)]
87 |
88 | (dom/footer :.footer {}
89 | (dom/span :.todo-count {}
90 | (dom/strong (str num-remaining " left")))
91 | (dom/ul :.filters {}
92 | (dom/li {}
93 | (dom/a {:className (when (or (nil? filter) (= :list.filter/none filter)) "selected")
94 | :href "#"} "All"))
95 | (dom/li {}
96 | (dom/a {:className (when (= :list.filter/active filter) "selected")
97 | :href "#/active"} "Active"))
98 | (dom/li {}
99 | (dom/a {:className (when (= :list.filter/completed filter) "selected")
100 | :href "#/completed"} "Completed")))
101 | (when (pos? num-completed)
102 | (dom/button {:className "clear-completed"
103 | :onClick #(comp/transact! component `[(api/todo-clear-complete {:list-id ~id})])} "Clear Completed")))))
104 |
105 | (defn footer-info []
106 | (dom/footer :.info {}
107 | (dom/p {} "Double-click to edit a todo")
108 | (dom/p {} "Created by "
109 | (dom/a {:href "http://www.fulcrologic.com"
110 | :target "_blank"} "Fulcrologic, LLC"))
111 | (dom/p {} "Part of "
112 | (dom/a {:href "http://todomvc.com"
113 | :target "_blank"} "TodoMVC"))))
114 |
115 | (defsc TodoList [this {:list/keys [id items filter title] :as props}]
116 | {:initial-state {:list/id 1 :ui/new-item-text "" :list/items [] :list/title "main" :list/filter :list.filter/none}
117 | :ident :list/id
118 | :query [:list/id :ui/new-item-text {:list/items (comp/get-query TodoItem)} :list/title :list/filter]}
119 | (let [num-todos (count items)
120 | completed-todos (filterv :item/complete items)
121 | num-completed (count completed-todos)
122 | all-completed? (= num-completed num-todos)
123 | filtered-todos (case filter
124 | :list.filter/active (filterv (comp not :item/complete) items)
125 | :list.filter/completed completed-todos
126 | items)
127 | delete-item (fn [item-id] (comp/transact! this `[(api/todo-delete-item ~{:list-id id :id item-id})]))
128 | check (fn [item-id] (comp/transact! this `[(api/todo-check ~{:id item-id})]))
129 | uncheck (fn [item-id] (comp/transact! this `[(api/todo-uncheck ~{:id item-id})]))]
130 | (log/info "Shared" (comp/shared this))
131 | (dom/div {}
132 | (dom/section :.todoapp {}
133 | (header this title)
134 | (when (pos? num-todos)
135 | (dom/div {}
136 | (dom/section :.main {}
137 | (dom/input {:type "checkbox"
138 | :className "toggle-all"
139 | :checked all-completed?
140 | :onClick (fn [] (if all-completed?
141 | (comp/transact! this `[(api/todo-uncheck-all {:list-id ~id})])
142 | (comp/transact! this `[(api/todo-check-all {:list-id ~id})])))})
143 | (dom/label {:htmlFor "toggle-all"} "Mark all as complete")
144 | (dom/ul :.todo-list {}
145 | (map #(ui-todo-item (comp/computed %
146 | {:delete-item delete-item
147 | :check check
148 | :uncheck uncheck})) filtered-todos)))
149 | (filter-footer this num-todos num-completed))))
150 | (footer-info))))
151 |
152 | (def ui-todo-list (comp/factory TodoList))
153 |
154 | (defsc Application [this {:keys [todos] :as props}]
155 | {:initial-state (fn [c p] {:route :application
156 | :todos (comp/get-initial-state TodoList {})})
157 | :ident (fn [] [:application :root])
158 | :query [:route {:todos (comp/get-query TodoList)}]}
159 | (dom/div {}
160 | (ui-todo-list todos)))
161 |
162 | (def ui-application (comp/factory Application))
163 |
164 | (defsc Other [this props]
165 | {:query [:route]
166 | :ident (fn [] [:other :root])
167 | :initial-state {:route :other}}
168 | (dom/div "OTHER ROUTE"))
169 |
170 | (fr/defsc-router TopRouter [this props]
171 | {:router-id ::top-router
172 | :ident (fn []
173 | [(:route props) :root])
174 | :default-route Application
175 | :router-targets {:application Application
176 | :other Other}})
177 |
178 | (def ui-router (comp/factory TopRouter))
179 |
180 | (defsc Root [this {:root/keys [router] :as props}]
181 | {:initial-state (fn [c p] {:root/router (comp/get-initial-state TopRouter {})})
182 | :query [{:root/router (comp/get-query TopRouter)}]}
183 | (log/info "root props" props)
184 | (dom/div {}
185 | (dom/button {:onClick (fn [] (comp/transact! this `[(fr/set-route {:router ::top-router :target [:application :root]})]))} "App")
186 | (dom/button {:onClick (fn [] (comp/transact! this `[(fr/set-route {:router ::top-router :target [:other :root]})]))} "Other")
187 | (ui-router router)))
188 |
--------------------------------------------------------------------------------