├── 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 | => "
hello
" 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 | => "

P

PS2

" 34 | "Component child in DOM with props" 35 | (render-to-str (dom/div {:className "test"} (ui-sample {}))) 36 | => "
Hello
" 37 | "Component child in DOM with kw shortcut" 38 | (render-to-str (dom/div :.TEST (ui-sample {}))) 39 | => "
Hello
" 40 | "works with threading macro" 41 | (render-to-str (->> 42 | (span "PS2") 43 | (p :.x) 44 | (div :.a#1 {:className "b"}))) 45 | => "

PS2

")) 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 | "

a

b

" 60 | 61 | "Props are optional" 62 | (dom/render-to-str (dom/div (comp/fragment (dom/p "a") (dom/p "b")))) 63 | => 64 | "

a

b

")) 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 | "
Hello
Hello
")) 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 | --------------------------------------------------------------------------------