diff --git a/.gitignore b/.gitignore index c13bd356f..e3c5fbb8e 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ compiled/ misc/ /examples/todomvc/resources/public/js/ /examples/simple/resources/public/js/ +figwheel_server.log diff --git a/examples/simple/checkouts/re-frame b/examples/simple/checkouts/re-frame new file mode 120000 index 000000000..1b20c9fb8 --- /dev/null +++ b/examples/simple/checkouts/re-frame @@ -0,0 +1 @@ +../../../ \ No newline at end of file diff --git a/examples/simple/devsrc/simpleexample/dev.cljs b/examples/simple/devsrc/simpleexample/dev.cljs index 543863a65..1e9d09063 100644 --- a/examples/simple/devsrc/simpleexample/dev.cljs +++ b/examples/simple/devsrc/simpleexample/dev.cljs @@ -1,6 +1,9 @@ (ns simpleexample.dev (:require [simpleexample.core :as example] - [figwheel.client :as fw])) + [figwheel.client :as fw] + [devtools.core :as devtools])) + +(devtools/install!) (fw/start {:on-jsload example/run :websocket-url "ws://localhost:3449/figwheel-ws"}) diff --git a/examples/simple/project.clj b/examples/simple/project.clj index 072390a9f..ab842ca39 100644 --- a/examples/simple/project.clj +++ b/examples/simple/project.clj @@ -1,32 +1,34 @@ (defproject simple-re-frame "0.4.0" - :dependencies [[org.clojure/clojure "1.6.0"] - [org.clojure/clojurescript "0.0-3208"] + :dependencies [[org.clojure/clojure "1.7.0"] + [org.clojure/clojurescript "1.7.107"] [reagent "0.5.0"] [re-frame "0.4.1"] - [figwheel "0.2.6"]] + [figwheel "0.3.7"] + [binaryage/devtools "0.3.0"]] :plugins [[lein-cljsbuild "1.0.5"] - [lein-figwheel "0.2.6"]] + [lein-figwheel "0.3.7"]] :hooks [leiningen.cljsbuild] - :profiles {:dev {:cljsbuild - {:builds {:client {:source-paths ["devsrc"] - :compiler {:main simpleexample.dev - :asset-path "js" - :optimizations :none - :source-map true - :source-map-timestamp true}}}}} + :profiles {:dev {:cljsbuild + {:builds {:client {:source-paths ["devsrc"] + :compiler {:main simpleexample.dev + :asset-path "js" + :optimizations :none + :source-map true + :source-map-timestamp true}}}}} :prod {:cljsbuild - {:builds {:client {:compiler {:optimizations :advanced - :elide-asserts true - :pretty-print false}}}}}} + {:builds {:client {:compiler {:optimizations :advanced + :elide-asserts true + :pretty-print false}}}}}} - :figwheel {:repl false} + :figwheel {:server-port 3440 + :nrepl-port 3540} :clean-targets ^{:protect false} ["resources/public/js"] - :cljsbuild {:builds {:client {:source-paths ["src"] + :cljsbuild {:builds {:client {:source-paths ["checkouts/re-frame/src" "src"] :compiler {:output-dir "resources/public/js" :output-to "resources/public/js/client.js"}}}}) diff --git a/examples/todomvc/checkouts/re-frame b/examples/todomvc/checkouts/re-frame new file mode 120000 index 000000000..a8a4f8c21 --- /dev/null +++ b/examples/todomvc/checkouts/re-frame @@ -0,0 +1 @@ +../../.. \ No newline at end of file diff --git a/examples/todomvc/devsrc/todomvc/dev.cljs b/examples/todomvc/devsrc/todomvc/dev.cljs index f1e54379f..b7878ad54 100644 --- a/examples/todomvc/devsrc/todomvc/dev.cljs +++ b/examples/todomvc/devsrc/todomvc/dev.cljs @@ -1,6 +1,9 @@ (ns todomvc.dev (:require [todomvc.core :as todomvc] - [figwheel.client :as fw])) + [figwheel.client :as fw] + [devtools.core :as devtools])) -(fw/start {:on-jsload todomvc/main +(devtools/install!) + +(fw/start {:on-jsload todomvc/main :websocket-url "ws://localhost:3450/figwheel-ws"}) diff --git a/examples/todomvc/project.clj b/examples/todomvc/project.clj index 40cb29e7c..277cdce84 100644 --- a/examples/todomvc/project.clj +++ b/examples/todomvc/project.clj @@ -1,36 +1,37 @@ (defproject todomvc-re-frame "0.4.0" - :dependencies [[org.clojure/clojure "1.6.0"] - [org.clojure/clojurescript "0.0-3208"] + :dependencies [[org.clojure/clojure "1.7.0"] + [org.clojure/clojurescript "1.7.107"] [reagent "0.5.0"] [re-frame "0.4.1"] - [figwheel "0.2.6"] + [figwheel "0.3.7"] [secretary "1.2.3"] - [prismatic/schema "0.4.3"]] + [prismatic/schema "0.4.3"] + [binaryage/devtools "0.3.0"]] :plugins [[lein-cljsbuild "1.0.5"] - [lein-figwheel "0.2.6"]] + [lein-figwheel "0.3.7"]] :hooks [leiningen.cljsbuild] - :profiles {:dev {:cljsbuild - {:builds {:client {:source-paths ["devsrc"] - :compiler {:main todomvc.dev - :asset-path "js" - :optimizations :none - :source-map true - :source-map-timestamp true}}}}} + :profiles {:dev {:cljsbuild + {:builds {:client {:source-paths ["devsrc"] + :compiler {:main todomvc.dev + :asset-path "js" + :optimizations :none + :source-map true + :source-map-timestamp true}}}}} :prod {:cljsbuild - {:builds {:client {:compiler {:optimizations :advanced - :elide-asserts true - :pretty-print false}}}}}} + {:builds {:client {:compiler {:optimizations :advanced + :elide-asserts true + :pretty-print false}}}}}} :figwheel {:server-port 3450 - :repl false} + :nrepl-port 3550} :clean-targets ^{:protect false} ["resources/public/js"] - :cljsbuild {:builds {:client {:source-paths ["src"] + :cljsbuild {:builds {:client {:source-paths ["checkouts/re-frame/src" "src"] :compiler {:output-dir "resources/public/js" :output-to "resources/public/js/client.js"}}}}) diff --git a/examples/todomvc/src/todomvc/core.cljs b/examples/todomvc/src/todomvc/core.cljs index 40af060c9..56752e3ac 100644 --- a/examples/todomvc/src/todomvc/core.cljs +++ b/examples/todomvc/src/todomvc/core.cljs @@ -1,7 +1,7 @@ (ns todomvc.core (:require-macros [secretary.core :refer [defroute]]) (:require [goog.events :as events] - [reagent.core :as reagent :refer [atom]] + [reagent.core :as reagent] [re-frame.core :refer [dispatch dispatch-sync]] [secretary.core :as secretary] [todomvc.handlers] @@ -21,7 +21,7 @@ (def history (doto (History.) (events/listen EventType.NAVIGATE - (fn [event] (secretary/dispatch! (.-token event)))) + (fn [event] (secretary/dispatch! (.-token event)))) (.setEnabled true))) @@ -31,4 +31,4 @@ [] (dispatch-sync [:initialise-db]) (reagent/render [todomvc.views/todo-app] - (.getElementById js/document "app"))) + (.getElementById js/document "app"))) diff --git a/project.clj b/project.clj index 623ce0053..cba38a54d 100644 --- a/project.clj +++ b/project.clj @@ -1,43 +1,39 @@ -(defproject re-frame "0.4.1" - :description "A Clojurescript MVC-like Framework For Writing SPAs Using Reagent." - :url "https://github.com/Day8/re-frame.git" - :license {:name "MIT"} - :dependencies [[org.clojure/clojure "1.6.0"] - [org.clojure/clojurescript "0.0-3211"] +(defproject re-frame "0.5.0-TRANSDUCERS" + :description "A Clojurescript MVC-like Framework For Writing SPAs Using Reagent." + :url "https://github.com/Day8/re-frame.git" + :license {:name "MIT"} + :dependencies [[org.clojure/clojure "1.7.0"] + [org.clojure/clojurescript "1.7.107"] [org.clojure/core.async "0.1.346.0-17112a-alpha"] [reagent "0.5.0"]] - :profiles {:debug {:debug true} - :dev {:dependencies [[spellhouse/clairvoyant "0.0-48-gf5e59d3"]] - :plugins [[lein-cljsbuild "1.0.5"] - [com.cemerick/clojurescript.test "0.3.3"]]}} + :profiles {:debug {:debug true} + :dev {:dependencies [[spellhouse/clairvoyant "0.0-48-gf5e59d3"]] + :plugins [[lein-cljsbuild "1.0.5"] + [com.cemerick/clojurescript.test "0.3.3"]]}} - - :clean-targets [:target-path - "run/compiled/demo"] + :clean-targets [:target-path "run/compiled"] :resource-paths ["run/resources"] - :jvm-opts ["-Xmx1g" "-XX:+UseConcMarkSweepGC"] ;; - :source-paths ["src"] - :test-paths ["test"] - - - :cljsbuild {:builds [{:id "test" ;; currently bogus, there is no demo or tests - :source-paths ["test"] - :compiler {:output-to "run/compiled/test.js" - :source-map "run/compiled/test.js.map" - :output-dir "run/compiled/test" - :optimizations :simple - :pretty-print true}}] - - :test-commands {"rhino" ["rhino" "-opt" "-1" :rhino-runner - "run/compiled/test.js"] - "slimer" ["xvfb-run" "-a" "slimerjs" :runner - "run/compiled/test.js"] - "phantom" ["phantomjs" ; doesn't work with phantomjs < 2.0.0 - :runner "run/compiled/test.js"]}} - - :aliases {"auto" ["do" "clean," "cljsbuild" "clean," "cljsbuild" "auto" "demo,"] - "once" ["do" "clean," "cljsbuild" "clean," "cljsbuild" "once" "demo,"] - "test-rhino" ["do" "clean," "cljsbuild" "once," "cljsbuild" "test" "rhino"] - "test-slimer" ["do" "clean," "cljsbuild" "once," "cljsbuild" "test" "slimer"] }) + :jvm-opts ["-Xmx1g" "-XX:+UseConcMarkSweepGC"] + :source-paths [] + :test-paths ["test"] + + + :cljsbuild {:builds [{:id "test" ;; currently bogus, there is no demo or tests + :source-paths ["src" "test"] + :compiler {:output-to "run/compiled/test.js" + :source-map "run/compiled/test.js.map" + :output-dir "run/compiled/test" + :optimizations :simple ;; https://github.com/cemerick/clojurescript.test/issues/68 + :pretty-print true}}] + + :test-commands {"rhino" ["rhino" "-opt" "-1" :rhino-runner "run/compiled/test.js"] + "slimer" ["xvfb-run" "-a" "slimerjs" :runner "run/compiled/test.js"] + "phantom" ["phantomjs" :runner "run/compiled/test.js"]}} ; doesn't work with phantomjs < 2.0.0 + + :aliases {"auto" ["do" "clean," "cljsbuild" "clean," "cljsbuild" "auto" "demo,"] + "once" ["do" "clean," "cljsbuild" "clean," "cljsbuild" "once" "demo,"] + "test-rhino" ["do" "clean," "cljsbuild" "once," "cljsbuild" "test" "rhino"] + "test-slimer" ["do" "clean," "cljsbuild" "once," "cljsbuild" "test" "slimer"] + "test-phantom" ["do" "clean," "cljsbuild" "once," "cljsbuild" "test" "phantom"]}) diff --git a/src/re_frame/core.cljs b/src/re_frame/core.cljs index 726c13558..4e6449140 100644 --- a/src/re_frame/core.cljs +++ b/src/re_frame/core.cljs @@ -1,56 +1,36 @@ (ns re-frame.core - (:require - [re-frame.handlers :as handlers] - [re-frame.subs :as subs] - [re-frame.router :as router] - [re-frame.utils :as utils] - [re-frame.middleware :as middleware])) + (:require [re-frame.scaffold :as scaffold])) +; this file provides public API to default re-frame setup +; note: by including this namespace, you will get default app-db, default app-frame +; and start default router-loop automatically -;; -- API ------- - -(def dispatch router/dispatch) -(def dispatch-sync router/dispatch-sync) - -(def register-sub subs/register) -(def clear-sub-handlers! subs/clear-handlers!) -(def subscribe subs/subscribe) - - -(def clear-event-handlers! handlers/clear-handlers!) - - -(def pure middleware/pure) -(def debug middleware/debug) -(def undoable middleware/undoable) -(def path middleware/path) -(def enrich middleware/enrich) -(def trim-v middleware/trim-v) -(def after middleware/after) -(def log-ex middleware/log-ex) - - -;; ALPHA - EXPERIMENTAL MIDDLEWARE -(def on-changes middleware/on-changes) - - -;; -- Logging ----- -;; re-frame uses the logging functions: warn, log, error, group and groupEnd -;; By default, these functions map directly to the js/console implementations -;; But you can override with your own (set or subset): -;; (set-loggers! {:warn my-warn :log my-looger ...}) -(def set-loggers! utils/set-loggers!) - - -;; -- Convenience API ------- - -;; Almost 100% of handlers will be pure, so make it easy to -;; register with "pure" middleware in the correct (left-hand-side) position. -(defn register-handler - ([id handler] - (handlers/register-base id pure handler)) - ([id middleware handler] - (handlers/register-base id [pure middleware] handler))) - +(def app-db scaffold/app-db) ; the default instance of app-db +(def app-frame scaffold/app-frame) ; the default instance of re-frame +;; -- API ------- +(def router-loop scaffold/router-loop) +(def set-loggers! scaffold/set-loggers!) +(def register-sub scaffold/register-sub) +(def unregister-sub scaffold/unregister-sub) +(def clear-sub-handlers! scaffold/clear-sub-handlers!) +(def subscribe scaffold/subscribe) +(def clear-event-handlers! scaffold/clear-event-handlers!) +(def dispatch scaffold/dispatch) +(def dispatch-sync scaffold/dispatch-sync) +(def register-handler scaffold/register-handler) +(def unregister-handler scaffold/unregister-handler) + +(def pure scaffold/pure) +(def debug scaffold/debug) +;(def undoable scaffold/undoable) +(def path scaffold/path) +(def enrich scaffold/enrich) +(def trim-v scaffold/trim-v) +(def after scaffold/after) +(def log-ex scaffold/log-ex) +(def on-changes scaffold/on-changes) + +;; start event processing +(scaffold/router-loop) diff --git a/src/re_frame/db.cljs b/src/re_frame/db.cljs deleted file mode 100644 index c6859b6fc..000000000 --- a/src/re_frame/db.cljs +++ /dev/null @@ -1,11 +0,0 @@ -(ns re-frame.db - (:require [reagent.core :as reagent])) - - -;; -- Application State -------------------------------------------------------------------------- -;; -;; Should not be accessed directly by application code -;; Read access goes through subscriptions. -;; Updates via event handlers. -(def app-db (reagent/atom {})) - diff --git a/src/re_frame/frame.cljs b/src/re_frame/frame.cljs new file mode 100644 index 000000000..6f98c2817 --- /dev/null +++ b/src/re_frame/frame.cljs @@ -0,0 +1,171 @@ +(ns re-frame.frame + (:require [re-frame.utils :refer [get-event-id get-subscription-id simple-inflection frame-summary-description reset-if-changed!]] + [re-frame.logging :refer [log warn error default-loggers]])) + +; re-frame meat implemented in terms of pure functions (with help of transducers) + +(defprotocol IFrame) + +(defrecord Frame [handlers subscriptions loggers] + IFrame) + +;; -- construction ----------------------------------------------------------------------------------------------------- + +(defn make-frame + "Constructs an independent frame instance." + ([] (make-frame nil)) + ([handlers] (make-frame handlers nil)) + ([handlers subscriptions] (make-frame handlers subscriptions default-loggers)) + ([handlers subscriptions loggers] + {:pre [(or (map? handlers) (nil? handlers)) + (or (map? subscriptions) (nil? subscriptions)) + (map? loggers)]} + (Frame. handlers subscriptions loggers))) + +;; -- event handlers --------------------------------------------------------------------------------------------------- + +(defn register-event-handler + "Register a handler for an event." + ([frame event-id handler-fn] + (let [existing-handlers (get frame :handlers)] + (if (contains? existing-handlers event-id) + (warn frame "re-frame: overwriting an event handler for: " event-id)) + (assoc-in frame [:handlers event-id] handler-fn)))) + +(defn unregister-event-handler + "Unregisters event handler function previously registered via register-event-handler." + [frame event-id] + (let [existing-handlers (get frame :handlers)] + (if (contains? existing-handlers event-id) + (warn frame "re-frame: unregistering event handler \"" event-id "\" which does not exist."))) + (update frame :handlers dissoc event-id)) + +(defn clear-event-handlers + "Unregisters all event handlers." + [frame] + (assoc frame :handlers nil)) + +;; -- subscriptions ---------------------------------------------------------------------------------------------------- + +(defn register-subscription-handler + "Registers a subscription handler function for an id." + [frame subscription-id handler-fn] + (let [existing-subscriptions (get frame :subscriptions)] + (if (contains? existing-subscriptions subscription-id) + (warn frame "re-frame: overwriting subscription handler for: " subscription-id))) + (assoc-in frame [:subscriptions subscription-id] handler-fn)) + +(defn unregister-subscription-handler + "Unregisters subscription handler function previously registered via register-subscription-handler." + [frame subscription-id] + (let [existing-subscriptions (get frame :subscriptions)] + (if-not (contains? existing-subscriptions subscription-id) + (warn frame "re-frame: unregistering subscription handler \"" subscription-id "\" which does not exist."))) + (update frame :subscriptions dissoc subscription-id)) + +(defn clear-subscription-handlers + "Unregisters all subscription handlers." + [frame] + (assoc frame :subscriptions nil)) + +(defn subscribe + "Returns a reagent/reaction which observes state." + [frame subscription-spec] + (let [subscription-id (get-subscription-id subscription-spec) + handler-fn (get-in frame [:subscriptions subscription-id])] + (if (nil? handler-fn) + (do + (error frame "re-frame: no subscription handler registered for: \"" subscription-id "\". Returning a nil subscription.") + nil) + (apply handler-fn subscription-spec)))) + +;; -- utilities -------------------------------------------------------------------------------------------------------- + +(defn set-loggers + "Resets loggers." + [frame new-loggers] + (assoc frame :loggers new-loggers)) + +;; -- transducers ------------------------------------------------------------------------------------------------------ +;; +;; see http://clojure.org/transducers[1] + +(defn frame-transducer-factory [frame] ; <- returns a function which is able to build transducers + (fn [db-selector] ; <- returns a transducer parametrized with db-selector + (fn [reducing-fn] ; <- this is a transducer + (fn + ([] (reducing-fn)) ; transduction init, see [1] + ([result] (reducing-fn result)) ; transduction completion, see [1] + ([state event] ; transduction step, see [1] + (let [event-id (get-event-id event) + handler-fn (event-id (:handlers frame))] + (if (nil? handler-fn) + (do + (error frame "re-frame: no event handler registered for: \"" event-id "\". Ignoring.") + state) + (let [old-db (db-selector state) ; db-selector is responsible for retrieving actual db from current state + new-db (handler-fn old-db event)] ; calls selected handler (including all composed middlewares) + (if (nil? new-db) ; TODO: this test should be optional, there could be valid use-cases for nil db + (do + (error frame "re-frame: your handler returned nil. It should return the new db state. Ignoring.") + state) + (reducing-fn state new-db)))))))))) ; reducing function prepares new transduction state + +; TODO: we should memoize this function, beause it will be usually called with same frame +; using something like https://github.com/clojure/core.memoize with LRU cache would be neat +(defn get-frame-transducer + "Returns a transducer: state, event -> state. +This transducer resolves event-id, selects matching handler and calls it with old db state to produce a new db state. +Transducer must have no knowledge of underlying app-db-atom, reagent, core.async or anything else out there. +All business of queuing events and application of their effects must be baked into reducing-fn and provided from outside +by the process doing actual transduction. See event processing helpers below for an example." + ([frame] (get-frame-transducer frame identity)) + ([frame db-selector] ((frame-transducer-factory frame) db-selector))) + +;; -- event processing ------------------------------------------------------------------------------------------------- + +(defn process-event [frame init-db event] + (let [reducing-fn (fn [_old-state new-state] new-state) + xform (get-frame-transducer frame identity)] + ((xform reducing-fn) init-db event))) + +(defn process-events [frame init-db events] + (let [reducing-fn (fn + ([db-states] db-states) ; completion + ([db-states new-db] ; in each step + (conj db-states new-db))) ; add new-db state to the vector + xform (get-frame-transducer frame last)] + (transduce xform reducing-fn [init-db] events))) + +(defn process-event-on-atom! [frame db-atom event] + (let [new-db (process-event frame @db-atom event)] + (reset-if-changed! db-atom new-db))) + +(defn process-events-on-atom! [frame db-atom events] + (let [reducing-fn (fn + ([db-atom] db-atom) ; completion + ([db-atom new-db] ; in each step + (reset-if-changed! db-atom new-db) ; commit new-db to atom + db-atom)) + xform (get-frame-transducer frame deref)] + (transduce xform reducing-fn db-atom events))) + +(defn process-events-on-atom-with-coallesced-write! [frame db-atom events] + (let [reducing-fn (fn + ([final-db] ; completion + (reset-if-changed! db-atom final-db)) ; commit final-db to atom + ([_old-db new-db] new-db)) ; simply carry-on with new-db as our new state + xform (get-frame-transducer frame identity)] + (transduce xform reducing-fn @db-atom events))) + +;; -- nice to have ----------------------------------------------------------------------------------------------------- + +(extend-protocol IPrintWithWriter + Frame + (-pr-writer [this writer opts] + (-write writer (str "#"))) diff --git a/src/re_frame/handlers.cljs b/src/re_frame/handlers.cljs deleted file mode 100644 index 7250e1cc0..000000000 --- a/src/re_frame/handlers.cljs +++ /dev/null @@ -1,94 +0,0 @@ -(ns re-frame.handlers - (:require [re-frame.db :refer [app-db]] - [re-frame.utils :refer [first-in-vector warn error]])) - - -;; -- composing middleware ----------------------------------------------------------------------- - - -(defn report-middleware-factories - "See https://github.com/Day8/re-frame/issues/65" - [v] - (letfn [(name-of-factory - [f] - (-> f meta :re-frame-factory-name)) - (factory-names-in - [v] - (remove nil? (map name-of-factory v)))] - (doseq [name (factory-names-in v)] - (error "re-frame: \"" name "\" used incorrectly. Must be used like this \"(" name " ...)\", whereas you just used \"" name "\".")))) - - -(defn comp-middleware - "Given a vector of middleware, filter out any nils, and use \"comp\" to compose the elements. - v can have nested vectors, and will be flattened before \"comp\" is applied. - For convienience, if v is a function (assumed to be middleware already), just return it. - Filtering out nils allows us to create Middleware conditionally like this: - (comp-middleware [pure (when debug? debug)]) ;; that 'when' might leave a nil - " - [v] - - (cond - (fn? v) v ;; assumed to be existing middleware - (seq? v) (let [v (remove nil? (flatten v))] - (report-middleware-factories v) - (apply comp v)) - :else (warn "re-frame: comp-middleware expects a vector, got: " v))) - - -;; -- the register of event handlers -------------------------------------------------------------- - -(def ^:private id->fn (atom {})) - - -(defn lookup-handler - [event-id] - (get @id->fn event-id)) - - -(defn clear-handlers! - "Unregister all event handlers" - [] - (reset! id->fn {})) - - -(defn register-base - "register a handler for an event. - This is low level and it is expected that \"re-frame.core/register-handler\" would - generally be used." - ([event-id handler-fn] - (when (contains? @id->fn event-id) - (warn "re-frame: overwriting an event-handler for: " event-id)) ;; allow it, but warn. - (swap! id->fn assoc event-id handler-fn)) - - ([event-id middleware handler-fn] - (let [mid-ware (comp-middleware middleware) ;; compose the middleware - midware+hfn (mid-ware handler-fn)] ;; wrap the handler in the middleware - (register-base event-id midware+hfn)))) - - - - -;; -- lookup and call ----------------------------------------------------------------------------- - -(def ^:dynamic *handling* nil) ;; remember what event we are currently handling - - -(defn handle - "Given an event vector, look up the handler, then call it. - By default, handlers are not assumed to be pure. They are called with - two paramters: - - the `app-db` atom - - the event vector - The handler is assumed to side-effect on `app-db` - the return value is ignored. - To write a pure handler, use the \"pure\" middleware when registering the handler." - [event-v] - (let [event-id (first-in-vector event-v) - handler-fn (lookup-handler event-id)] - (if (nil? handler-fn) - (error "re-frame: no event handler registered for: \"" event-id "\". Ignoring.") - (if *handling* - (error "re-frame: while handling \"" *handling* "\" dispatch-sync was called for \"" event-v "\". You can't call dispatch-sync in an event handler.") - (binding [*handling* event-v] - (handler-fn app-db event-v)))))) - diff --git a/src/re_frame/legacy.cljs b/src/re_frame/legacy.cljs new file mode 100644 index 000000000..c297a0250 --- /dev/null +++ b/src/re_frame/legacy.cljs @@ -0,0 +1,33 @@ +(ns re-frame.legacy + (:require [reagent.ratom :refer [IReactiveAtom]] + [re-frame.logging :refer [log warn error group group-end]])) + +;; See docs in the Wiki: https://github.com/Day8/re-frame/wiki + +; this middleware is included for backward compatibility, it is not used anymore +(defn pure + "Acts as an adaptor, allowing handlers to be writen as pure functions. + The re-frame router passes the `app-db` atom as the first parameter to any handler. + This middleware adapts that atom to be the value within the atom. + If you strip away the error/efficiency checks, this middleware is doing: + (reset! app-db (handler @app-db event-vec)) + You don't have to use this middleware directly. It is automatically applied to + your handler's middleware when you use \"register-handler\". + In fact, the only way to by-pass automatic use of \"pure\" in your middleware + is to use the low level registration function \"re-frame.handlers/register-handler-base\"" + [frame-atom] + (fn [handler] + (fn pure-handler [app-db event-vec] + (warn @frame-atom "re-frame: pure handler should not be used anymore.") + (if (not (satisfies? IReactiveAtom app-db)) + (do + (if (map? app-db) + (warn @frame-atom "re-frame: Looks like \"pure\" is in the middleware pipeline twice. Ignoring.") + (warn @frame-atom "re-frame: \"pure\" middleware not given a Ratom. Got: " app-db)) + handler) ;; turn this into a noop handler + (let [db @app-db + new-db (handler db event-vec)] + (if (nil? new-db) + (error @frame-atom "re-frame: your pure handler returned nil. It should return the new db state.") + (if-not (identical? db new-db) + (reset! app-db new-db)))))))) diff --git a/src/re_frame/logging.cljs b/src/re_frame/logging.cljs new file mode 100644 index 000000000..aa0b6afaa --- /dev/null +++ b/src/re_frame/logging.cljs @@ -0,0 +1,57 @@ +(ns re-frame.logging) + +(defn make-logger-for-key [logger-key] + (fn [frame & args] + (if-let [logger-fn (get-in frame [:loggers logger-key])] + (apply logger-fn args) + (throw (js/Error. (str "re-frame: missing logger \"" logger-key "\"")))))) + +; logging helpers +(def log (make-logger-for-key :log)) +(def warn (make-logger-for-key :warn)) +(def error (make-logger-for-key :error)) +(def group (make-logger-for-key :group)) +(def group-end (make-logger-for-key :groupEnd)) + +(defn no-op [& _]) + +(defn js-console-log [& args] + (.apply (.-log js/console) js/console (into-array args))) + +(defn js-console-warn [& args] + (.apply (.-warn js/console) js/console (into-array args))) + +(defn js-console-error [& args] + (.apply (.-error js/console) js/console (into-array args))) + +(defn js-console-group [& args] + (if (.-group js/console) ;; group does not exist < IE 11 + (.apply (.-group js/console) js/console (into-array args)) + (apply js-console-log args))) + +(defn js-console-group-end [& args] + (if (.-groupEnd js/console) ;; groupEnd does not exist < IE 11 + (.apply (.-groupEnd js/console) js/console (into-array args)))) + +;; -- Logging ----------------------------------------------------------------- +;; +;; re-frame internally uses a set of logging functions which, by default, +;; print to js/console. +;; Use set-loggers! if you want to change this default behaviour. +;; In production environment, you may want to capture exceptions and POST +;; them somewhere. to , you might want to override the way that exceptions are +;; handled by overridding "error" +;; +(def default-loggers + {:log js-console-log + :warn js-console-warn + :error js-console-error + :group js-console-group + :groupEnd js-console-group-end}) + +(def no-loggers + {:log no-op + :warn no-op + :error no-op + :group no-op + :groupEnd no-op}) diff --git a/src/re_frame/middleware.cljs b/src/re_frame/middleware.cljs index 0ce0085c9..a9db14d86 100644 --- a/src/re_frame/middleware.cljs +++ b/src/re_frame/middleware.cljs @@ -1,41 +1,10 @@ (ns re-frame.middleware - (:require - [reagent.ratom :refer [IReactiveAtom]] - [re-frame.undo :refer [store-now!]] - [re-frame.utils :refer [warn log group groupEnd error]] - [clojure.data :as data])) + (:require [clojure.data :as data] + [re-frame.logging :refer [log warn error group group-end]])) ;; See docs in the Wiki: https://github.com/Day8/re-frame/wiki - -(defn pure - "Acts as an adaptor, allowing handlers to be writen as pure functions. - The re-frame router passes the `app-db` atom as the first parameter to any handler. - This middleware adapts that atom to be the value within the atom. - If you strip away the error/efficiency checks, this middleware is doing: - (reset! app-db (handler @app-db event-vec)) - You don't have to use this middleware directly. It is automatically applied to - your handler's middleware when you use \"register-handler\". - In fact, the only way to by-pass automatic use of \"pure\" in your middleware - is to use the low level registration function \"re-frame.handlers/register-handler-base\"" - [handler] - (fn pure-handler - [app-db event-vec] - (if (not (satisfies? IReactiveAtom app-db)) - (do - (if (map? app-db) - (warn "re-frame: Looks like \"pure\" is in the middleware pipeline twice. Ignoring.") - (warn "re-frame: \"pure\" middleware not given a Ratom. Got: " app-db)) - handler) ;; turn this into a noop handler - (let [db @app-db - new-db (handler db event-vec)] - (if (nil? new-db) - (error "re-frame: your pure handler returned nil. It should return the new db state.") - (if-not (identical? db new-db) - (reset! app-db new-db))))))) - - (defn log-ex "Middleware which catches and prints any handler-generated exceptions to console. Handlers are called from within a core.async go-loop, and core.async produces @@ -45,33 +14,35 @@ So this middleware catches and prints to stacktrace before the core.async sausage machine has done its work. " - [handler] - (fn log-ex-handler - [db v] - (warn "re-frame: use of \"log-ex\" is deprecated. You don't need it any more IF YOU ARE USING CHROME 44. Chrome now seems to now produce good stack traces.") - (try - (handler db v) - (catch :default e ;; ooops, handler threw - (do - (.error js/console (.-stack e)) - (throw e)))))) + [frame-atom] + (fn [handler] + (fn log-ex-handler + [db v] + (warn @frame-atom "re-frame: use of \"log-ex\" is deprecated. You don't need it any more IF YOU ARE USING CHROME 44. Chrome now seems to now produce good stack traces.") + (try + (handler db v) + (catch :default e ;; ooops, handler threw + (do + (.error js/console (.-stack e)) + (throw e))))))) (defn debug "Middleware which logs debug information to js/console for each event. Includes a clojure.data/diff of the db, before vs after, showing the changes caused by the event." - [handler] - (fn debug-handler - [db v] - (log "-- New Event ----------------------------------------------------") - (group "re-frame event: " v) - (let [new-db (handler db v) - diff (data/diff db new-db)] - (log "only before: " (first diff)) - (log "only after : " (second diff)) - (groupEnd) - new-db))) + [frame-atom] + (fn [handler] + (fn debug-handler [db v] + (let [frame @frame-atom] + (log frame "-- New Event ----------------------------------------------------") + (group frame "re-frame event: " v) + (let [new-db (handler db v) + diff (data/diff db new-db)] + (log frame "only before: " (first diff)) + (log frame "only after : " (second diff)) + (group-end frame) + new-db))))) @@ -83,10 +54,11 @@ [db [x y z]] ;; <-- instead of [_ x y z] ....) " - [handler] - (fn trim-v-handler - [db v] - (handler db (vec (rest v))))) + [_frame-atom] + (fn [handler] + (fn trim-v-handler + [db v] + (handler db (vec (rest v)))))) ;; -- Middleware Factories ---------------------------------------------------- @@ -101,7 +73,7 @@ ;; ;; So, yeah, weird. -(def path +(defn path "A middleware factory which supplies a sub-tree of `db` to the handler. Works a bit like update-in. Supplies a narrowed data structure for the handler. Afterwards, grafts the result of the handler back into db. @@ -111,12 +83,13 @@ (path [:some :path] :to :here) (path [:some :path] [:to] :here) " - ^{:re-frame-factory-name "path"} + [frame-atom] + ;^{:re-frame-factory-name "path"} (fn path [& args] - (let [path (flatten args)] + (let [path (flatten args)] (when (empty? path) - (error "re-frame: \"path\" middleware given no params.")) + (error @frame-atom "re-frame: \"path\" middleware given no params.")) (fn path-middleware [handler] (fn path-handler @@ -124,29 +97,29 @@ (assoc-in db path (handler (get-in db path) v))))))) -(def undoable - "A Middleware factory which stores an undo checkpoint. - \"explanation\" can be either a string or a function. If it is a - function then must be: (db event-vec) -> string. - \"explanation\" can be nil. in which case \"\" is recorded. - " - ^{:re-frame-factory-name "undoable"} - (fn undoable - [explanation] - (fn undoable-middleware - [handler] - (fn undoable-handler - [db event-vec] - (let [explanation (cond - (fn? explanation) (explanation db event-vec) - (string? explanation) explanation - (nil? explanation) "" - :else (error "re-frame: \"undoable\" middleware given a bad parameter. Got: " explanation))] - (store-now! explanation) - (handler db event-vec)))))) - - -(def enrich +#_(def undoable + "A Middleware factory which stores an undo checkpoint. + \"explanation\" can be either a string or a function. If it is a + function then must be: (db event-vec) -> string. + \"explanation\" can be nil. in which case \"\" is recorded. + " + ^{:re-frame-factory-name "undoable"} + (fn undoable + [explanation] + (fn undoable-middleware + [handler] + (fn undoable-handler + [db event-vec] + (let [explanation (cond + (fn? explanation) (explanation db event-vec) + (string? explanation) explanation + (nil? explanation) "" + :else (error "re-frame: \"undoable\" middleware given a bad parameter. Got: " explanation))] + (store-now! explanation) + (handler db event-vec)))))) + + +(defn enrich "Middleware factory which runs a given function \"f\" in the after position. \"f\" is (db v) -> db Unlike \"after\" which is about side effects, \"enrich\" expects f to process and alter @@ -164,7 +137,8 @@ \"f\" would need to be both adding and removing the duplicate warnings. By applying \"f\" in middleware, we keep the handlers simple and yet we ensure this important step is not missed." - ^{:re-frame-factory-name "enrich"} + [_frame-atom] + ;^{:re-frame-factory-name "enrich"} (fn enrich [f] (fn enrich-middleware @@ -174,14 +148,15 @@ (f (handler db v) v))))) -(def after +(defn after "Middleware factory which runs a function \"f\" in the \"after handler\" position presumably for side effects. \"f\" is given the new value of \"db\". It's return value is ignored. Examples: \"f\" can run schema validation. Or write current state to localstorage. etc. In effect, \"f\" is meant to sideeffect. It gets no chance to change db. See \"enrich\" (if you need that.)" - ^{:re-frame-factory-name "after"} + [_frame-atom] + ;^{:re-frame-factory-name "after"} (fn after [f] (fn after-middleware @@ -189,13 +164,13 @@ (fn after-handler [db v] (let [new-db (handler db v)] - (f new-db v) ;; call f for side effects + (f new-db v) ;; call f for side effects new-db))))) ;; EXPERIMENTAL -(def on-changes +(defn on-changes "Middleware factory which acts a bit like \"reaction\" (but it flows into db , rather than out) It observes N inputs (paths into db) and if any of them change (as a result of the handler being run) then it runs 'f' to compute a new value, which is @@ -215,19 +190,20 @@ - call 'f' with the values extracted from [:a] [:b] - assoc the return value from 'f' into the path [:c] " - ^{:re-frame-factory-name "on-changes"} + [_frame-atom] + ;^{:re-frame-factory-name "on-changes"} (fn on-changes [f out-path & in-paths] (fn on-changed-middleware [handler] (fn on-changed-handler [db v] - (let [ ;; run the handler, computing a new generation of db + (let [;; run the handler, computing a new generation of db new-db (handler db v) ;; work out if any "inputs" have changed - new-ins (map #(get-in new-db %) in-paths) - old-ins (map #(get-in db %) in-paths) + new-ins (map #(get-in new-db %) in-paths) + old-ins (map #(get-in db %) in-paths) changed-ins? (some false? (map identical? new-ins old-ins))] ;; if one of the inputs has changed, then run 'f' diff --git a/src/re_frame/router.cljs b/src/re_frame/router.cljs deleted file mode 100644 index b4f6186a3..000000000 --- a/src/re_frame/router.cljs +++ /dev/null @@ -1,93 +0,0 @@ -(ns re-frame.router - (:refer-clojure :exclude [flush]) - (:require-macros [cljs.core.async.macros :refer [go-loop go]]) - (:require [reagent.core :refer [flush]] - [re-frame.handlers :refer [handle]] - [re-frame.utils :refer [warn error]] - [cljs.core.async :refer [chan put! fn (atom {})) - - -(defn clear-handlers! - "Unregisters all subscription handlers" - [] - (reset! key->fn {})) - - -(defn register - "Registers a handler function for an id" - [key-v handler-fn] - (if (contains? @key->fn key-v) - (warn "re-frame: overwriting subscription-handler for: " key-v)) ;; allow it, but warn. - (swap! key->fn assoc key-v handler-fn)) - - -(defn subscribe - "Returns a reagent/reaction which observes a part of app-db" - [v] - (let [key-v (first-in-vector v) - handler-fn (get @key->fn key-v)] - (if (nil? handler-fn) - (error "re-frame: no subscription handler registered for: \"" key-v "\". Returning a nil subscription.") - (handler-fn app-db v)))) - diff --git a/src/re_frame/undo.cljs b/src/re_frame/undo.cljs deleted file mode 100644 index 32f1519d8..000000000 --- a/src/re_frame/undo.cljs +++ /dev/null @@ -1,173 +0,0 @@ -(ns re-frame.undo - (:require-macros [reagent.ratom :refer [reaction]]) - (:require - [reagent.core :as reagent] - [re-frame.utils :refer [warn]] - [re-frame.db :refer [app-db]] - [re-frame.handlers :as handlers] - [re-frame.subs :as subs])) - - -;; -- History ------------------------------------------------------------------------------------- -;; -;; -(def ^:private max-undos "Maximum number of undo states maintained" (atom 50)) -(defn set-max-undos! - [n] - (reset! max-undos n)) - - -(def ^:private undo-list "A list of history states" (reagent/atom [])) -(def ^:private redo-list "A list of future states, caused by undoing" (reagent/atom [])) - -;; -- Explanations ----------------------------------------------------------- -;; -;; Each undo has an associated explanation which can be displayed to the user. -;; -;; Seems really ugly to have mirrored vectors, but ... -;; the code kinda falls out when you do. I'm feeling lazy. -(def ^:private app-explain "Mirrors app-db" (reagent/atom "")) -(def ^:private undo-explain-list "Mirrors undo-list" (reagent/atom [])) -(def ^:private redo-explain-list "Mirrors redo-list" (reagent/atom [])) - -(defn- clear-undos! - [] - (reset! undo-list []) - (reset! undo-explain-list [])) - - -(defn- clear-redos! - [] - (reset! redo-list []) - (reset! redo-explain-list [])) - - -(defn clear-history! - [] - (clear-undos!) - (clear-redos!) - (reset! app-explain "")) - - -(defn store-now! - "Stores the value currently in app-db, so the user can later undo" - [explanation] - (clear-redos!) - (reset! undo-list (vec (take - @max-undos - (conj @undo-list @app-db)))) - (reset! undo-explain-list (vec (take - @max-undos - (conj @undo-explain-list @app-explain)))) - (reset! app-explain explanation)) - - -(defn undos? - "Returns true if undos exist, false otherwise" - [] - (pos? (count @undo-list))) - -(defn redos? - "Returns true if redos exist, false otherwise" - [] - (pos? (count @redo-list))) - -(defn undo-explanations - "Returns list of undo descriptions or empty list if no undos" - [] - (if (undos?) - (conj @undo-explain-list @app-explain) - [])) - -;; -- subscriptions ----------------------------------------------------------------------------- - -(subs/register - :undos? - (fn handler - ; "return true if anything is stored in the undo list, otherwise false" - [_ _] - (reaction (undos?)))) - -(subs/register - :redos? - (fn handler - ; "return true if anything is stored in the redo list, otherwise false" - [_ _] - (reaction (redos?)))) - - -(subs/register - :undo-explanations - (fn handler - ; "return a vector of string explanations ordered oldest to most recent" - [_ _] - (reaction (undo-explanations)))) - -(subs/register - :redo-explanations - (fn handler - ; "returns a vector of string explanations ordered from most recent undo onward" - [_ _] - (reaction (deref redo-explain-list)))) - -;; -- event handlers ---------------------------------------------------------------------------- - - -(defn- undo - [undos cur redos] - (let [u @undos - r (cons @cur @redos)] - (reset! cur (last u)) - (reset! redos r) - (reset! undos (pop u)))) - -(defn- undo-n - "undo n steps or until we run out of undos" - [n] - (when (and (pos? n) (undos?)) - (undo undo-list app-db redo-list) - (undo undo-explain-list app-explain redo-explain-list) - (recur (dec n)))) - -(handlers/register-base ;; not a pure handler - :undo ;; usage: (dispatch [:undo n]) n is optional, defaults to 1 - (fn handler - [_ [_ n]] - (if-not (undos?) - (warn "re-frame: you did a (dispatch [:undo]), but there is nothing to undo.") - (undo-n (or n 1))))) - - -(defn- redo - [undos cur redos] - (let [u (conj @undos @cur) - r @redos] - (reset! cur (first r)) - (reset! redos (rest r)) - (reset! undos u))) - -(defn- redo-n - "redo n steps or until we run out of redos" - [n] - (when (and (pos? n) (redos?)) - (redo undo-list app-db redo-list) - (redo undo-explain-list app-explain redo-explain-list) - (recur (dec n)))) - -(handlers/register-base ;; not a pure handler - :redo ;; usage: (dispatch [:redo n]) - (fn handler ;; if n absent, defaults to 1 - [_ [_ n]] - (if-not (redos?) - (warn "re-frame: you did a (dispatch [:redo]), but there is nothing to redo.") - (redo-n (or n 1))))) - - -(handlers/register-base ;; not a pure handler - :purge-redos ;; usage: (dispatch [:purge-redo]) - (fn handler - [_ _] - (if-not (redos?) - (warn "re-frame: you did a (dispatch [:purge-redos]), but there is nothing to redo.") - (clear-redos!)))) - diff --git a/src/re_frame/utils.cljs b/src/re_frame/utils.cljs index 9bf6f20fd..907fc0c76 100644 --- a/src/re_frame/utils.cljs +++ b/src/re_frame/utils.cljs @@ -1,46 +1,60 @@ (ns re-frame.utils - (:require - [clojure.set :refer [difference]])) - - -;; -- Logging ----------------------------------------------------------------- -;; -;; re-frame internally uses a set of logging functions which, by default, -;; print to js/console. -;; Use set-loggers! if you want to change this default behaviour. -;; In production environment, you may want to capture exceptions and POST -;; them somewhere. to , you might want to override the way that exceptions are -;; handled by overridding "error" -;; -(def default-loggers - {:log #(.log js/console %) - :warn #(.warn js/console %) - :error #(.error js/console %) - :group #(if (.group js/console) (.group js/console %) (.log js/console %)) ;; group does not exist < IE 11 - :groupEnd #(when (.groupEnd js/console) (.groupEnd js/console))}) ;; groupEnd does not exist < IE 11 - -;; holds the current set of loggers. -(def loggers (atom default-loggers)) - -(defn set-loggers! - "Change the set (subset?) of logging functions used by re-frame. - 'new-loggers' should be a map which looks like default-loggers" - [new-loggers] - (assert (empty? (difference (set (keys new-loggers)) (set (keys default-loggers)))) "Unknown keys in new-loggers") - (swap! loggers merge new-loggers)) - - -(defn log [& args] ((:log @loggers) (apply str args))) -(defn warn [& args] ((:warn @loggers) (apply str args))) -(defn group [& args] ((:group @loggers) (apply str args))) -(defn groupEnd [& args] ((:groupEnd @loggers) (apply str args))) -(defn error [& args] ((:error @loggers) (apply str args))) - -;; -- Misc -------------------------------------------------------------------- - -(defn first-in-vector + (:require [re-frame.logging :refer [log warn error]])) + +(defn get-event-id [v] (if (vector? v) (first v) - (error "re-frame: expected a vector event, but got: " v))) + (throw (js/Error. (str "re-frame: expected a vector event, but got: " v))))) +(defn get-subscription-id + [v] + (if (vector? v) + (first v) + (throw (js/Error. (str "re-frame: expected a vector subscription, but got: " v))))) + +(defn simple-inflection [base n] + (if (= n 1) base (str base "s"))) + +(defn frame-summary-description [frame] + (let [handlers-count (count (:handlers frame)) + subscriptions-count (count (:subscriptions frame))] + (str + handlers-count " " (simple-inflection "handler" handlers-count) ", " + subscriptions-count " " (simple-inflection "subscription" subscriptions-count)))) + +(defn reset-if-changed! [db-atom new-db-state] + (if-not (identical? @db-atom new-db-state) + (reset! db-atom new-db-state))) + +;; -- composing middleware ----------------------------------------------------------------------- + +(defn report-middleware-factories + "See https://github.com/Day8/re-frame/issues/65" + [frame v] + (letfn [(name-of-factory + [f] + (-> f meta :re-frame-factory-name)) + (factory-names-in + [v] + (remove nil? (map name-of-factory v)))] + (doseq [name (factory-names-in v)] + (error frame "re-frame: \"" name "\" used incorrectly. Must be used like this \"(" name " ...)\", whereas you just used \"" name "\".")))) + +(defn compose-middleware + "Given a vector of middleware, filter out any nils, and use \"comp\" to compose the elements. + v can have nested vectors, and will be flattened before \"comp\" is applied. + For convienience, if v is a function (assumed to be middleware already), just return it. + Filtering out nils allows us to create Middleware conditionally like this: + (comp-middleware [pure (when debug? debug)]) ;; that 'when' might leave a nil + " + [frame what] + (let [spec (if (seqable? what) (seq what) what)] + (cond + (fn? spec) spec ;; assumed to be existing middleware + (seq? spec) (let [middlewares (remove nil? (flatten spec))] + (report-middleware-factories frame middlewares) + (apply comp middlewares)) + :else (do + (warn frame "re-frame: comp-middleware expects a vector, got: " what) + nil)))) diff --git a/test/re-frame/middleware.cljs b/test/re-frame/middleware.cljs deleted file mode 100644 index 12f07abcb..000000000 --- a/test/re-frame/middleware.cljs +++ /dev/null @@ -1,41 +0,0 @@ -(ns re-frame.test.middleware - (:require-macros [cemerick.cljs.test :refer (is deftest)]) - (:require [cemerick.cljs.test :as t] - [reagent.ratom :refer [atom]] - [re-frame.middleware :as middleware])) - -(enable-console-print!) - -(deftest pure - (let [db (atom {:a true}) - handler (fn [db [_ key value]] - (assoc db key value))] - ((middleware/pure handler) db [nil :a false]) - (is (= (:a @db) false)))) - -(deftest trim-v - (let [handler (fn [db vect] vect)] - (is (= (handler nil [:a :b :c]) - [:a :b :c])) - (is (= ((middleware/trim-v handler) nil [:a :b :c]) - [:b :c])))) - -(deftest path - (let [db {:a true} - handler (fn - [a [_]] - (not a))] - (let [new-db (((middleware/path [:a]) - handler) db [nil])] - (is (= (:a new-db) - false))))) - - -(deftest on-changes - (let [set-a (fn [db v] (assoc db :a v)) ;; handler - mid-ware (middleware/on-changes + [:c] [:a] [:b]) ;; middleware - wrapped (mid-ware set-a)] ;; wrapped middleware - (is (= (wrapped {:a 0 :b 2} 0) ;; no change in 'a' - {:a 0 :b 2})) - (is (= (wrapped {:a 4 :b 2} 0) ;; 'a' changed to 0 - {:c 2 :a 0 :b 2})))) ;; 'c' is a + b diff --git a/test/re_frame/test/core.cljs b/test/re_frame/test/core.cljs new file mode 100644 index 000000000..067be78d7 --- /dev/null +++ b/test/re_frame/test/core.cljs @@ -0,0 +1,50 @@ +(ns re-frame.test.core + (:require-macros [cemerick.cljs.test :refer (is deftest testing done)] + [reagent.ratom :refer [reaction run!]]) + (:require [cemerick.cljs.test] + [re-frame.core :as core] + [re-frame.frame :as frame])) + +(defn reinitialize! [] + ; TODO: figure out, how to force channel flush + (reset! core/app-db nil) + (reset! core/app-frame (frame/make-frame))) + +(deftest modify-app-db-sync + (testing "modify app-db via handler (sync)" + (reinitialize!) + (is (= @core/app-db nil)) + (core/register-handler :modify-app (fn [db [_ data]] + (assoc db :modify-app-handler-was-here data))) + (core/dispatch-sync [:modify-app "something"]) + (is (= @core/app-db {:modify-app-handler-was-here "something"})))) + +(deftest ^:async modify-app-db-async + (testing "modify app-db via handler (async)" + (reinitialize!) + (is (= @core/app-db nil)) + (core/register-handler :modify-app (fn [db [_ data]] + (assoc db :modify-app-handler-was-here data))) + (core/register-handler :check (fn [db] + (is (= db @core/app-db)) + (is (= db {:modify-app-handler-was-here "something"})) + (done) + db)) + (core/dispatch [:modify-app "something"]) + (core/dispatch [:check]))) + +(deftest subscribing + (testing "register subscription handler and trigger it" + (reinitialize!) + (reset! core/app-db 0) + (let [target (atom nil) + db-adder (fn [db [sub-id num]] + (is (= sub-id :db-adder)) + (reaction (+ @db num))) + _ (core/register-sub :db-adder db-adder) + subscription (core/subscribe [:db-adder 10])] + (is (= @target nil)) + (run! (reset! target @subscription)) + (is (= @target 10)) + (swap! core/app-db inc) + (is (= @target 11))))) diff --git a/test/re_frame/test/frame.cljs b/test/re_frame/test/frame.cljs new file mode 100644 index 000000000..6f08ed3bb --- /dev/null +++ b/test/re_frame/test/frame.cljs @@ -0,0 +1,143 @@ +(ns re-frame.test.frame + (:require-macros [cemerick.cljs.test :refer (is deftest testing)] + [reagent.ratom :refer [reaction run!]]) + (:require [cemerick.cljs.test] + [re-frame.frame :as frame] + [re-frame.test.utils.log-recording :refer [recording-loggers reset-log-recorder! last-error last-warn last-log]] + [reagent.core :as reagent])) + +(defn make-test-frame-with-log-recording [] + (frame/make-frame nil nil recording-loggers)) + +(deftest frame-errors + (testing "doing invalid subscription" + (reset-log-recorder!) + (let [frame (make-test-frame-with-log-recording)] + (is (thrown-with-msg? js/Error #"expected a vector subscription, but got:" (frame/subscribe frame :non-vector))))) + (testing "subscribing to a non-existent subscription handler" + (reset-log-recorder!) + (let [frame (make-test-frame-with-log-recording)] + (is (= (last-error) nil)) + (frame/subscribe frame [:subscription-which-does-not-exist]) + (is (= (last-error) ["re-frame: no subscription handler registered for: \"" :subscription-which-does-not-exist "\". Returning a nil subscription."])))) + (testing "calling a handler which returns nil" + (reset-log-recorder!) + (let [my-handler (fn [_state _] nil) + frame (-> (make-test-frame-with-log-recording) + (frame/register-event-handler :my-handler my-handler))] + (is (= (last-error) nil)) + (is (= (frame/process-event frame nil [:my-handler]) nil)) + (is (= (last-error) ["re-frame: your handler returned nil. It should return the new db state. Ignoring."])))) + (testing "calling a handler which does not exist" + (reset-log-recorder!) + (let [frame (make-test-frame-with-log-recording)] + (is (= (last-error) nil)) + (is (= (frame/process-event frame nil [:non-existing-handler]) nil)) + (is (= (last-error) ["re-frame: no event handler registered for: \"" :non-existing-handler "\". Ignoring."]))))) + +(deftest frame-warnings + (testing "overwriting subscription handler" + (reset-log-recorder!) + (let [frame (make-test-frame-with-log-recording) + frame-with-some-handler (frame/register-subscription-handler frame :some-handler identity)] + (is (= (last-warn) nil)) + (frame/register-subscription-handler frame-with-some-handler :some-handler (fn [])) + (is (= (last-warn) ["re-frame: overwriting subscription handler for: " :some-handler])))) + (testing "overwriting event handler" + (reset-log-recorder!) + (let [frame (make-test-frame-with-log-recording) + frame-with-some-handler (frame/register-event-handler frame :some-handler identity)] + (is (= (last-warn) nil)) + (frame/register-event-handler frame-with-some-handler :some-handler (fn [])) + (is (= (last-warn) ["re-frame: overwriting an event handler for: " :some-handler])))) + (testing "unregistering subscription handler which does not exist" + (reset-log-recorder!) + (let [frame (make-test-frame-with-log-recording)] + (is (= (last-warn) nil)) + (frame/unregister-subscription-handler frame :non-existing-handler) + (is (= (last-warn) ["re-frame: unregistering subscription handler \"" :non-existing-handler "\" which does not exist."]))))) + +(deftest frame-event-handlers + (testing "unregister event handler" + (let [frame (-> (make-test-frame-with-log-recording) + (frame/register-event-handler :some-handler identity))] + (is (= (get-in frame [:handlers :some-handler]) identity)) + (is (= (get-in (frame/unregister-event-handler frame :some-handler) [:handlers :some-handler] ::not-found) ::not-found)))) + (testing "clear event handlers" + (let [frame (-> (make-test-frame-with-log-recording) + (frame/register-event-handler :some-handler identity))] + (is (= (count (get frame :handlers)) 1)) + (is (= (count (get (frame/clear-event-handlers frame) :handlers)) 0))))) + +(deftest frame-subscription-handlers + (testing "unregister subscription handler" + (let [frame (-> (make-test-frame-with-log-recording) + (frame/register-subscription-handler :some-handler identity))] + (is (= (get-in frame [:subscriptions :some-handler]) identity)) + (is (= (get-in (frame/unregister-subscription-handler frame :some-handler) [:subscriptions :some-handler] ::not-found) ::not-found)))) + (testing "clear subscription handlers" + (let [frame (-> (make-test-frame-with-log-recording) + (frame/register-subscription-handler :some-handler identity))] + (is (= (count (get frame :subscriptions)) 1)) + (is (= (count (get (frame/clear-subscription-handlers frame) :subscriptions)) 0))))) + +(deftest frame-processing-events + (testing "process event" + (let [my-handler (fn [state [& args]] (str "state:" state " args:" args)) + frame (-> (make-test-frame-with-log-recording) + (frame/register-event-handler :my-handler my-handler)) + result (frame/process-event frame "[initial state]" [:my-handler 1 2])] + (is (= result "state:[initial state] args:(:my-handler 1 2)")))) + (testing "process multiple events, get vector of states back" + (let [init-db 0 + add-handler (fn [state [_event-id num]] (+ state num)) + frame (-> (make-test-frame-with-log-recording) + (frame/register-event-handler :add add-handler)) + result (frame/process-events frame init-db [[:add 1] [:add 2] [:add 10]])] + (is (= result [0 1 3 13])))) + (testing "process event on atom" + (let [db (atom 0) + reset-counter (atom 0) + _ (add-watch db ::watcher #(swap! reset-counter inc)) + add-handler (fn [state [_event-id num]] (+ state num)) + frame (-> (make-test-frame-with-log-recording) + (frame/register-event-handler :add add-handler))] + (frame/process-event-on-atom! frame db [:add 100]) + (is (= @db 100)) + (is (= @reset-counter 1)))) + (testing "process multiple events on atom" + (let [db (atom 0) + reset-counter (atom 0) + _ (add-watch db ::watcher #(swap! reset-counter inc)) + add-handler (fn [state [_event-id num]] (+ state num)) + frame (-> (make-test-frame-with-log-recording) + (frame/register-event-handler :add add-handler))] + (frame/process-events-on-atom! frame db [[:add 1] [:add 2]]) + (is (= @db 3)) + (is (= @reset-counter 2)))) + (testing "process multiple events on atom with coallesced write" + (let [db (atom 0) + reset-counter (atom 0) + _ (add-watch db ::watcher #(swap! reset-counter inc)) + add-handler (fn [state [_event-id num]] (+ state num)) + frame (-> (make-test-frame-with-log-recording) + (frame/register-event-handler :add add-handler))] + (frame/process-events-on-atom-with-coallesced-write! frame db [[:add 1] [:add 2]]) + (is (= @db 3)) + (is (= @reset-counter 1))))) + +(deftest frame-subscriptions + (testing "register subscription handler and trigger it" + (let [source (reagent/atom 0) + target (atom nil) + source-adder (fn [sub-id num] + (is (= sub-id :source-adder)) + (reaction (+ @source num))) + frame (-> (make-test-frame-with-log-recording) + (frame/register-subscription-handler :source-adder source-adder)) + subscription (frame/subscribe frame [:source-adder 10])] + (is (= @target nil)) + (run! (reset! target @subscription)) + (is (= @target 10)) + (swap! source inc) + (is (= @target 11))))) diff --git a/test/re_frame/test/logging.cljs b/test/re_frame/test/logging.cljs new file mode 100644 index 000000000..47a1a7700 --- /dev/null +++ b/test/re_frame/test/logging.cljs @@ -0,0 +1,10 @@ +(ns re-frame.test.logging + (:require-macros [cemerick.cljs.test :refer (is deftest testing)]) + (:require [cemerick.cljs.test] + [re-frame.frame :as frame] + [re-frame.logging :as logging])) + +(deftest frame-logging + (testing "supplying incomplete logger" + (let [frame (frame/make-frame nil nil {})] + (is (thrown-with-msg? js/Error #"re-frame: missing logger \":log\"" (logging/log frame "log this!")))))) diff --git a/test/re_frame/test/middleware.cljs b/test/re_frame/test/middleware.cljs new file mode 100644 index 000000000..1062870b8 --- /dev/null +++ b/test/re_frame/test/middleware.cljs @@ -0,0 +1,32 @@ +(ns re-frame.test.middleware + (:require-macros [cemerick.cljs.test :refer (is deftest)]) + (:require [cemerick.cljs.test] + [reagent.ratom :refer [atom]] + [re-frame.core :as re-frame])) + +(deftest trim-v + (let [handler (fn [db vect] vect)] + (is (= (handler nil [:a :b :c]) + [:a :b :c])) + (is (= ((re-frame/trim-v handler) nil [:a :b :c]) + [:b :c])))) + +(deftest path + (let [db {:a true} + handler (fn + [a [_]] + (not a))] + (let [new-db (((re-frame/path [:a]) + handler) db [nil])] + (is (= (:a new-db) + false))))) + + +(deftest on-changes + (let [set-a (fn [db v] (assoc db :a v)) ;; handler + mid-ware (re-frame/on-changes + [:c] [:a] [:b]) ;; api + wrapped (mid-ware set-a)] ;; wrapped api + (is (= (wrapped {:a 0 :b 2} 0) ;; no change in 'a' + {:a 0 :b 2})) + (is (= (wrapped {:a 4 :b 2} 0) ;; 'a' changed to 0 + {:c 2 :a 0 :b 2})))) ;; 'c' is a + b diff --git a/test/re_frame/test/utils/log_recording.cljs b/test/re_frame/test/utils/log_recording.cljs new file mode 100644 index 000000000..bd22af15a --- /dev/null +++ b/test/re_frame/test/utils/log_recording.cljs @@ -0,0 +1,27 @@ +(ns re-frame.test.utils.log-recording) + +(def log-transcript (atom {})) + +(defn record-log-call [key args] + (swap! log-transcript (fn [transcript] + (update transcript key + (fn [record] + (-> (or record {}) + (update :counter inc) + (update :history conj (vec args)))))))) + +(defn last-log* [key] + (last (get-in @log-transcript [key :history]))) + +(def last-log (partial last-log* :log)) +(def last-warn (partial last-log* :warn)) +(def last-error (partial last-log* :error)) + +(defn make-log-recorder-for-key [key] + (fn [& args] (record-log-call key args))) + +(def recording-loggers + (into {} (map (fn [key] [key (make-log-recorder-for-key key)]) [:log :warn :error :group :groupEnd]))) + +(defn reset-log-recorder! [] + (reset! log-transcript {}))