This commit is contained in:
Stuart Mitchell 2015-03-06 17:34:31 +13:00
commit 2551c6fcb4
6 changed files with 207 additions and 57 deletions

71
CHANGES.md Normal file
View File

@ -0,0 +1,71 @@
## Planned for v0.3.0
- automatically wrap subscriptions in a `reaction` (removing the need for over
10 keystrokes per handler!!). Just kidding there are better reasons than that.
- produce a more fully featured todomvc (beyond the standard one), todomvc-with-extras
- use `enrich` to handle todo duplication
- show testing
- show debug
- begin to use goog.Logger ?? How to let client apps know about exceptions, etc ??
## v0.2.0 (2015-03-06)
Being blocked by:
In todomvc, to be done tomorrow ahead of release:
- fix bug in todomvc which means the footer disappears and doesn't come back.
- add localstorage
- add history, back button etc.
### Breaking
Renames:
- `register-pure-handler` renamed to `register-handler` (and existing low level `register-handler` becomes `register-handler-base` but is not a part of the API).
- remove `apply-event` middleware and replace with similar `trim-v`
- rename `register-subs` to `register-sub` (avoid confusion over possible plurals)
- rename `set-max-undos` to `set-max-undos!`
Changes:
- `undoable` middleware is now a factory. Where before you used this `undoable`,
you must now use this `(undoable "some explanation")`. See further below.
### Headline
- exceptions in handler now reported more sanely via console.error.
(core.async really messes with a good stack)
- example `todomvc` available in examples folder.
- Wiki documentation is now more substantial.
- introduce new handler middleware: `debug`, `enrich` and `after`
### Other:
- exceptions in a go loop are a special type of hell. Improve the reporting of exceptions happening in event handlers.
- allow Middleware to be registered as a vector. data > functions > macros
- fix two bugs in undo implementation
- name licence file correctly, thanks to @smith
- fix typo in readme, thanks to @btheado
- Readme now admits to 200 lines of code, not 100.
- dispatch now explicitly returns nil
- travis integration (not that we have any tests currently)
### Details On Undo Changes
The undo/redo feature built into re-frame is now more functional
(at the cost of a breaking change).
There is now an explanation associated with each undo state describing
modification. This allows an app to inform the user what actions
they will be undoing or redoing.
Previously `undoable` was simply middleware, but it is now a middleware factory.
Essentially, that means you can't use it "plain" anymore, and instead you must
call it, like this `(undoable "Some explanation")`
The `explanation` provided to undoable must be either a `string` (static
explanation) or a function `(db event) -> string`, allowing you to customize
the undo message based on details of the event.

View File

@ -34,15 +34,15 @@
check-schema ;; middleware
(fn [_ _] ;; the handler
(merge default-value
(get-local-storage)))) ;; all hail the new state
#_(get-local-storage)))) ;; all hail the new state
(register-handler ;; handlers changes the footer filter
:set-showing ;; event-id
[check-schema debug trim-v] ;; middleware (wraps the handler)
[(path [:showing]) #_write-ls check-schema debug trim-v] ;; middleware (wraps the handler)
(fn ;; handler
[db [filter-kw]]
(assoc db :showing filter-kw)))
filter-kw))
(register-handler ;; given the text, create a new todo

View File

@ -65,5 +65,13 @@
handler-fn (lookup-handler event-id)]
(if (nil? handler-fn)
(warn "re-frame: no event handler registered for: \"" event-id "\". Ignoring.") ;; TODO: make exception
(handler-fn app-db event-v))))
(try
(handler-fn app-db event-v)
(catch :default e
(do
;; use of a core.async loop seems to completely ruin exception stacks.
;; So we're going print the exception to the console here, before it gets trashed.
(.error js/console e)
(throw e)))))))

View File

@ -11,24 +11,29 @@
(defn pure
"Acts as an adaptor, allowing handlers to be writen as pure functions.
The re-frame router will pass in an atom as the first parameter. This middleware
adapts that to the value within the atom.
If you strip away the error/efficiency checks, this middleware is just:
(reset! app-db (handler @app-db event-vec))"
The re-frame router will pass in an atom as the first parameter. This
middleware adapts that atom to be the value within the atom.
If you strip away the error/efficiency checks, this middleware is just doing:
(reset! app-db (handler @app-db event-vec))
You don't have to use this middleare directly. Is supplied by default
when you use register-handler.
The only way to by-pass use of pure is to use the low level registration function
re-frame.handlers/register-handler-base"
[handler]
(fn pure-handler
[app-db event-vec]
(assert (satisfies? IReactiveAtom app-db)
(str "re-frame: pure not given a Ratom."
(if (map? app-db)
" Looks like \"pure\" is in the middleware pipeline twice."
(str " Got: " app-db))))
(let [orig-db @app-db
new-db (handler orig-db event-vec)]
(if (nil? new-db)
(warn "re-frame: your pure handler returned nil. It should return the new db.")
(if-not (identical? orig-db new-db)
(reset! app-db new-db))))))
(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 [orig-db @app-db
new-db (handler orig-db event-vec)]
(if (nil? new-db)
(warn "re-frame: your pure handler returned nil. It should return the new db state.")
(if-not (identical? orig-db new-db)
(reset! app-db new-db)))))))
(defn debug
@ -38,25 +43,15 @@
[handler]
(fn debug-handler
[db v]
(if (satisfies? IReactiveAtom db)
(str "re-frame: \"debug\" middleware used without prior \"pure\"."))
(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))
(log "only after : " (second diff))
(groupEnd)
new-db)))
(defn undoable
"Middleware which stores an undo checkpoint."
[handler]
(fn undoable-handler
[app-db event-vec]
(store-now!)
(handler app-db event-vec)))
(defn trim-v
"Middleware which removes the first element of v which allows you to write
@ -73,27 +68,46 @@
(defn path
"Supplies a sub-tree of `db` to the handler. A narrowed view.
"A middleware factory which supplies a sub-tree of `db` to the handler.
Supplies a narrowed view.
Assumes \"pure\" is in the middleware pipeline prior.
Grafts the result back into db.
If a get-in of the path results in a nil, then \"default-fn\" will be called to supply a value.
XXX very like update-in. Should the name be more indicative of that closeness? "
([p]
(path p hash-map))
(path p nil))
([p default-fn]
(fn middleware
[handler]
(fn path-handler
[db v]
(if (satisfies? IReactiveAtom db)
(str "re-frame: \"path\" used in middleware, without prior \"pure\"."))
(if-not (vector? p)
(warn "re-frame: \"path\" expected a vector, got: " p))
(let [val (get-in db p)
val (if (nil? val) (default-fn) val)]
val (if (and (nil? val) (fn? default-fn)) (default-fn) val)]
(assoc-in db p (handler val v)))))))
(defn undoable
"A Middleware factory which stores an undo checkpoint.
\"explanation\" can be either a string or a function. If it is a
function then it must be: (db event-vec) -> string.
\"explanation\" can be nil. in which case no
"
[explanation]
(fn middleware
[handler]
(fn undoable-handler
[db event-vec]
(let [explanation (cond
(fn? explanation) (explanation db event-vec)
(string? explanation) explanation
(nil? explanation) ""
:else (warn "re-frame: undoable given 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) -> db
@ -126,7 +140,8 @@
position presumably for side effects.
\"f\" is given the 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 \"derive\" (if you need that.)"
In effect, \"f\" is meant to sideeffect. It gets no chance to change db. See \"enrich\"
(if you need that.)"
[f]
(fn middleware
[handler]

View File

@ -32,10 +32,7 @@
_ (if (:flush-dom (meta event-v)) ;; check the event for metadata
(do (flush) (<! (timeout 20))) ;; wait just over one annimation frame (16ms), to rensure all pending GUI work is flushed to the DOM.
(<! (timeout 0)))] ;; just in case we are handling one dispatch after an other, give the browser back control to do its stuff
(try
(handle event-v) ;; XXX There should be a plugable place to write exceptions and warns
(catch js/Object e
(.error js/console e))) ;; exceptions are a special kind of rubbish when you are in a goloop. So catch it here and print.
(handle event-v)
(recur)))
@ -55,8 +52,13 @@
(defn dispatch-sync
"Invoke the event handler sycronously, avoiding the async-inducing use of core.async/chan"
"Send an event to be processed by the registered handler, but avoid the async-inducing
use of core.async/chan.
Usage example:
(dispatch-sync [:delete-item 42])"
[event-v]
(handle event-v))
(handle event-v)
nil) ;; Ensure nil return. See https://github.com/Day8/re-frame/wiki/Returning-False

View File

@ -1,38 +1,56 @@
(ns re-frame.undo
(:require-macros [reagent.ratom :refer [reaction]])
(:require
[re-frame.utils :refer [warn]]
[reagent.core :as reagent]
[re-frame.db :refer [app-db]]
[re-frame.handlers :as handlers]
[re-frame.subs :as subs ]))
[re-frame.subs :as subs]))
;; -- History -------------------------------------------------------------------------------------
;;
;;
(def ^:private max-undos (atom 50)) ;; maximum number of undo states maintained
(defn set-max-undos
(defn set-max-undos!
[n]
(reset! max-undos n))
;;
(def ^:private undo-list (reagent/atom [])) ;; a list of history states
(def ^:private redo-list (reagent/atom [])) ;; a list of future states, caused by undoing
;; -- Explainations -----------------------------------------------------------
;;
;; 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 (reagent/atom "")) ;; mirrors app-db
(def ^:private undo-explain-list (reagent/atom [])) ;; mirrors undo-liat
(def ^:private redo-explain-list (reagent/atom [])) ;; mirrors redo-list
(defn clear-history!
[]
(reset! undo-list [])
(reset! redo-list []))
(reset! redo-list [])
(reset! undo-explain-list [])
(reset! redo-explain-list []))
(defn store-now!
"stores the value currently in app-db, so the user can later undo"
[]
[explanation]
(reset! redo-list []) ;; clear and redo state created by previous undos
(reset! redo-explain-list []) ;; clear and redo state created by previous undos
(reset! undo-list (vec (take
@max-undos
(conj @undo-list @app-db)))))
(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?
[]
@ -42,7 +60,6 @@
[]
(pos? (count @redo-list)))
;; -- subscriptions -----------------------------------------------------------------------------
(subs/register
@ -60,23 +77,60 @@
(reaction (redos?))))
(subs/register
:undo-explantions
(fn handler
; "returns a vector of string explnations
[_ _]
(reaction (deref undo-explain-list))))
(subs/register
:redo-explantions
(fn handler
; "returns a vector of string explnations
[_ _]
(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))))
(handlers/register-base ;; not a pure handler
:undo ;; usage: (dispatch [:undo])
(fn handler
[_ _]
(when (undos?)
(reset! redo-list (cons @app-db @redo-list))
(reset! app-db (last @undo-list))
(reset! undo-list (pop @undo-list)))))
(if-not (undos?)
(warn "re-frame: you did a (dispatch [:undo]), but there is nothing to undo.")
(do
(undo undo-list app-db redo-list)
(undo undo-explain-list app-explain redo-explain-list)))))
(defn- redo
[undos cur redos]
(let [u (conj @undos @cur)
r @redos]
(reset! cur (first r))
(reset! redos (rest r))
(reset! undos u)))
(handlers/register-base ;; not a pure handler
:redo ;; usage: (dispatch [:redo])
(fn handler
[_ _]
(when (redos?)
(reset! app-db (first @redo-list))
(reset! redo-list (rest @redo-list))
(reset! undo-list (conj @undo-list @app-db)))))
(if-not (redos?)
(warn "re-frame: you did a (dispatch [:redo]), but there is nothing to redo.")
(do
(redo undo-list app-db redo-list)
(redo undo-explain-list app-explain redo-explain-list)))))