WIP. undo test fails.
This commit is contained in:
parent
fd50562811
commit
9452998e0b
15
CHANGES.md
15
CHANGES.md
|
@ -26,8 +26,10 @@ Headline:
|
|||
|
||||
XXX link to more docs.
|
||||
|
||||
- the API for the undo/redo framework has been documented. It existed previously, but it
|
||||
was not officially there.
|
||||
- the API for the undo/redo features have been documented and put into `re-frame.core`.
|
||||
Most of the features existed previously, but now more official.
|
||||
|
||||
One feature is new: the ability to undo/redo just part of the `app-db` tree.
|
||||
https://github.com/Day8/re-frame/wiki/Undo-&-Redo
|
||||
|
||||
|
||||
|
@ -57,9 +59,12 @@ Breaking:
|
|||
hook in your own loggers. Otherwise, you have nothing to do.
|
||||
|
||||
Improvements
|
||||
- XXX (full-debug!)
|
||||
- XXXX middleware for spec checking of event vectors
|
||||
- XXX todomvc split into simple and advanced.
|
||||
- XXX What name for reg-pure-sub (too long)
|
||||
- XXX review todomvc views
|
||||
- XXX (full-debug!)
|
||||
- XXX middleware for spec checking of event vectors
|
||||
- XXX todomvc changed to use spc, instead of Schema
|
||||
- XXX todomvc split into simple and advanced.
|
||||
|
||||
- Bug fix: `post-event-callbacks` were not called when `dispatch-sync` was called.
|
||||
- added new API `re-frame.core/remove-post-event-callback`. See doc string.
|
||||
|
|
|
@ -44,33 +44,33 @@ input[type="checkbox"] {
|
|||
display: none;
|
||||
}
|
||||
|
||||
.todoapp {
|
||||
#todoapp {
|
||||
background: #fff;
|
||||
margin: 130px 0 40px 0;
|
||||
position: relative;
|
||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
|
||||
0 25px 50px 0 rgba(0, 0, 0, 0.1);
|
||||
0 25px 50px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.todoapp input::-webkit-input-placeholder {
|
||||
#todoapp input::-webkit-input-placeholder {
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
color: #e6e6e6;
|
||||
}
|
||||
|
||||
.todoapp input::-moz-placeholder {
|
||||
#todoapp input::-moz-placeholder {
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
color: #e6e6e6;
|
||||
}
|
||||
|
||||
.todoapp input::input-placeholder {
|
||||
#todoapp input::input-placeholder {
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
color: #e6e6e6;
|
||||
}
|
||||
|
||||
.todoapp h1 {
|
||||
#todoapp h1 {
|
||||
position: absolute;
|
||||
top: -155px;
|
||||
width: 100%;
|
||||
|
@ -83,7 +83,7 @@ input[type="checkbox"] {
|
|||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
.new-todo,
|
||||
#new-todo,
|
||||
.edit {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
|
@ -104,14 +104,14 @@ input[type="checkbox"] {
|
|||
font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
.new-todo {
|
||||
#new-todo {
|
||||
padding: 16px 16px 16px 60px;
|
||||
border: none;
|
||||
background: rgba(0, 0, 0, 0.003);
|
||||
box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
|
||||
}
|
||||
|
||||
.main {
|
||||
#main {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
border-top: 1px solid #e6e6e6;
|
||||
|
@ -121,7 +121,7 @@ label[for='toggle-all'] {
|
|||
display: none;
|
||||
}
|
||||
|
||||
.toggle-all {
|
||||
#toggle-all {
|
||||
position: absolute;
|
||||
top: -55px;
|
||||
left: -12px;
|
||||
|
@ -131,50 +131,50 @@ label[for='toggle-all'] {
|
|||
border: none; /* Mobile Safari */
|
||||
}
|
||||
|
||||
.toggle-all:before {
|
||||
#toggle-all:before {
|
||||
content: '❯';
|
||||
font-size: 22px;
|
||||
color: #e6e6e6;
|
||||
padding: 10px 27px 10px 27px;
|
||||
}
|
||||
|
||||
.toggle-all:checked:before {
|
||||
#toggle-all:checked:before {
|
||||
color: #737373;
|
||||
}
|
||||
|
||||
.todo-list {
|
||||
#todo-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.todo-list li {
|
||||
#todo-list li {
|
||||
position: relative;
|
||||
font-size: 24px;
|
||||
border-bottom: 1px solid #ededed;
|
||||
}
|
||||
|
||||
.todo-list li:last-child {
|
||||
#todo-list li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.todo-list li.editing {
|
||||
#todo-list li.editing {
|
||||
border-bottom: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.todo-list li.editing .edit {
|
||||
#todo-list li.editing .edit {
|
||||
display: block;
|
||||
width: 506px;
|
||||
padding: 13px 17px 12px 17px;
|
||||
margin: 0 0 0 43px;
|
||||
}
|
||||
|
||||
.todo-list li.editing .view {
|
||||
#todo-list li.editing .view {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.todo-list li .toggle {
|
||||
#todo-list li .toggle {
|
||||
text-align: center;
|
||||
width: 40px;
|
||||
/* auto, since non-WebKit browsers doesn't support input styling */
|
||||
|
@ -188,17 +188,17 @@ label[for='toggle-all'] {
|
|||
appearance: none;
|
||||
}
|
||||
|
||||
.todo-list li .toggle:after {
|
||||
#todo-list li .toggle:after {
|
||||
content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#ededed" stroke-width="3"/></svg>');
|
||||
}
|
||||
|
||||
.todo-list li .toggle:checked:after {
|
||||
#todo-list li .toggle:checked:after {
|
||||
content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#bddad5" stroke-width="3"/><path fill="#5dc2af" d="M72 25L42 71 27 56l-4 4 20 20 34-52z"/></svg>');
|
||||
}
|
||||
|
||||
.todo-list li label {
|
||||
white-space: pre;
|
||||
word-break: break-word;
|
||||
#todo-list li label {
|
||||
white-space: pre-line;
|
||||
word-break: break-all;
|
||||
padding: 15px 60px 15px 15px;
|
||||
margin-left: 45px;
|
||||
display: block;
|
||||
|
@ -206,12 +206,12 @@ label[for='toggle-all'] {
|
|||
transition: color 0.4s;
|
||||
}
|
||||
|
||||
.todo-list li.completed label {
|
||||
#todo-list li.completed label {
|
||||
color: #d9d9d9;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.todo-list li .destroy {
|
||||
#todo-list li .destroy {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
@ -226,27 +226,27 @@ label[for='toggle-all'] {
|
|||
transition: color 0.2s ease-out;
|
||||
}
|
||||
|
||||
.todo-list li .destroy:hover {
|
||||
#todo-list li .destroy:hover {
|
||||
color: #af5b5e;
|
||||
}
|
||||
|
||||
.todo-list li .destroy:after {
|
||||
#todo-list li .destroy:after {
|
||||
content: '×';
|
||||
}
|
||||
|
||||
.todo-list li:hover .destroy {
|
||||
#todo-list li:hover .destroy {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.todo-list li .edit {
|
||||
#todo-list li .edit {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.todo-list li.editing:last-child {
|
||||
#todo-list li.editing:last-child {
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
#footer {
|
||||
color: #777;
|
||||
padding: 10px 15px;
|
||||
height: 20px;
|
||||
|
@ -254,7 +254,7 @@ label[for='toggle-all'] {
|
|||
border-top: 1px solid #e6e6e6;
|
||||
}
|
||||
|
||||
.footer:before {
|
||||
#footer:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 0;
|
||||
|
@ -263,22 +263,22 @@ label[for='toggle-all'] {
|
|||
height: 50px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
|
||||
0 8px 0 -3px #f6f6f6,
|
||||
0 9px 1px -3px rgba(0, 0, 0, 0.2),
|
||||
0 16px 0 -6px #f6f6f6,
|
||||
0 17px 2px -6px rgba(0, 0, 0, 0.2);
|
||||
0 8px 0 -3px #f6f6f6,
|
||||
0 9px 1px -3px rgba(0, 0, 0, 0.2),
|
||||
0 16px 0 -6px #f6f6f6,
|
||||
0 17px 2px -6px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.todo-count {
|
||||
#todo-count {
|
||||
float: left;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.todo-count strong {
|
||||
#todo-count strong {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.filters {
|
||||
#filters {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
|
@ -287,11 +287,11 @@ label[for='toggle-all'] {
|
|||
left: 0;
|
||||
}
|
||||
|
||||
.filters li {
|
||||
#filters li {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.filters li a {
|
||||
#filters li a {
|
||||
color: inherit;
|
||||
margin: 3px;
|
||||
padding: 3px 7px;
|
||||
|
@ -300,17 +300,17 @@ label[for='toggle-all'] {
|
|||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.filters li a.selected,
|
||||
.filters li a:hover {
|
||||
#filters li a.selected,
|
||||
#filters li a:hover {
|
||||
border-color: rgba(175, 47, 47, 0.1);
|
||||
}
|
||||
|
||||
.filters li a.selected {
|
||||
#filters li a.selected {
|
||||
border-color: rgba(175, 47, 47, 0.2);
|
||||
}
|
||||
|
||||
.clear-completed,
|
||||
html .clear-completed:active {
|
||||
#clear-completed,
|
||||
html #clear-completed:active {
|
||||
float: right;
|
||||
position: relative;
|
||||
line-height: 20px;
|
||||
|
@ -319,11 +319,11 @@ html .clear-completed:active {
|
|||
position: relative;
|
||||
}
|
||||
|
||||
.clear-completed:hover {
|
||||
#clear-completed:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.info {
|
||||
#info {
|
||||
margin: 65px auto 0;
|
||||
color: #bfbfbf;
|
||||
font-size: 10px;
|
||||
|
@ -331,17 +331,17 @@ html .clear-completed:active {
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
.info p {
|
||||
#info p {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.info a {
|
||||
#info a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.info a:hover {
|
||||
#info a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
|
@ -350,16 +350,16 @@ html .clear-completed:active {
|
|||
Can't use it globally since it destroys checkboxes in Firefox
|
||||
*/
|
||||
@media screen and (-webkit-min-device-pixel-ratio:0) {
|
||||
.toggle-all,
|
||||
.todo-list li .toggle {
|
||||
#toggle-all,
|
||||
#todo-list li .toggle {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.todo-list li .toggle {
|
||||
#todo-list li .toggle {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.toggle-all {
|
||||
#toggle-all {
|
||||
-webkit-transform: rotate(90deg);
|
||||
transform: rotate(90deg);
|
||||
-webkit-appearance: none;
|
||||
|
@ -368,11 +368,12 @@ html .clear-completed:active {
|
|||
}
|
||||
|
||||
@media (max-width: 430px) {
|
||||
.footer {
|
||||
#footer {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.filters {
|
||||
#filters {
|
||||
bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -13,8 +13,8 @@
|
|||
|
||||
|
||||
;; -- Debugging aids ----------------------------------------------------------
|
||||
(devtools/install!) ;; we love https://github.com/binaryage/cljs-devtools
|
||||
(enable-console-print!)
|
||||
(devtools/install!) ;; we love https://github.com/binaryage/cljs-devtools
|
||||
(enable-console-print!) ;; so println writes to console.log
|
||||
|
||||
;; -- Routes and History ------------------------------------------------------
|
||||
|
||||
|
|
|
@ -1,51 +1,147 @@
|
|||
(ns todomvc.subs
|
||||
(:require-macros [reagent.ratom :refer [reaction]])
|
||||
(:require [re-frame.core :refer [register-sub]]))
|
||||
(:require [re-frame.core :refer [register-pure-sub subscribe]]))
|
||||
|
||||
;; register-pure-sub allows us to write subscription handlers without ever
|
||||
;; using `reaction` directly.
|
||||
;; This is how you would register a simple handler.
|
||||
(register-pure-sub
|
||||
:showing
|
||||
(fn [db _] ;; db, is the value in app-db
|
||||
(:showing db))) ;; I repeat: db is a value. Not a ratom. And this fn does not return a reaction, just a value.
|
||||
|
||||
;; -- Helpers -----------------------------------------------------------------
|
||||
;; that `fn` is a pure function
|
||||
|
||||
;; Next, the registration of a similar handler is done in two steps.
|
||||
;; First, we `defn` a pure handler function. Then, we use `register-pure-sub` to register it.
|
||||
;; Two steps. This is different to the first registration, which was done in one step.
|
||||
(defn sorted-todos
|
||||
[db _]
|
||||
(:todos db))
|
||||
(register-pure-sub :sorted-todos sorted-todos)
|
||||
|
||||
(defn filter-fn-for
|
||||
[showing-kw]
|
||||
(case showing-kw
|
||||
:active (complement :done)
|
||||
:done :done
|
||||
:all identity))
|
||||
;; -------------------------------------------------------------------------------------
|
||||
;; Beyond Simple Handlers
|
||||
;;
|
||||
;; A subscription handler is a function which is re-run when its input signals
|
||||
;; change. Each time it is rerun, it produces a new output (return value).
|
||||
;;
|
||||
;; In the simple case, app-db is the only input signal, as was the case in the two
|
||||
;; simple subscriptions above. But many subscriptions are not directly dependent on
|
||||
;; app-db, and instead, depend on a value derived from app-db.
|
||||
;;
|
||||
;; Such handlers represent "intermediate nodes" in a signal graph. New values emanate
|
||||
;; from app-db, and flow out through a signal graph, into and out of these intermediate
|
||||
;; nodes, before a leaf subscription delivers data into views which render data as hiccup.
|
||||
;;
|
||||
;; When writing and registering the handler for an intermediate node, you must nominate
|
||||
;; one or more input signals (typically one or two).
|
||||
;;
|
||||
;; register-pure-sub allows you to supply:
|
||||
;;
|
||||
;; 1. a function which returns the input signals. It can return either a single signal or
|
||||
;; a vector of signals, or a map of where the values are the signals.
|
||||
;;
|
||||
;; 2. a function which does the computation. It takes input values and produces a new
|
||||
;; derived value.
|
||||
;;
|
||||
;; In the two simple examples at the top, we only supplied the 2nd of these functions.
|
||||
;; But now we are dealing with intermediate nodes, we'll need to provide both fns.
|
||||
;;
|
||||
(register-pure-sub
|
||||
:todos
|
||||
|
||||
;; This function returns the input signals.
|
||||
;; In this case, it returns a single signal.
|
||||
;; Although not required in this example, it is called with two paramters
|
||||
;; being the two values supplied in the originating `(subscribe X Y)`.
|
||||
;; X will be the query vector and Y is an advanced feature and out of scope
|
||||
;; for this explanation.
|
||||
(fn [query-v _]
|
||||
(subscribe [:sorted-todos])) ;; returns a single input signal
|
||||
|
||||
(defn completed-count
|
||||
"return the count of todos for which :done is true"
|
||||
[todos]
|
||||
(count (filter :done (vals todos))))
|
||||
;; This 2nd fn does the computation. Data values in, derived data out.
|
||||
;; It is the same as the two simple subscription handlers up at the top.
|
||||
;; Except they took the value in app-db as their first argument and, instead,
|
||||
;; this function takes the value delivered by another input signal, supplied by the
|
||||
;; function above: (subscribe [:sorted-todos])
|
||||
;;
|
||||
;; Subscription handlers can take 3 parameters:
|
||||
;; - the input signals (a single item, a vector or a map)
|
||||
;; - the query vector supplied to query-v (the query vector argument
|
||||
;; to the "subscribe") and the 3rd one is for advanced cases, out of scope for this discussion.
|
||||
(fn [sorted-todos query-v _]
|
||||
(vals sorted-todos)))
|
||||
|
||||
|
||||
;; -- Subscription handlers and registration ---------------------------------
|
||||
|
||||
(register-sub
|
||||
:todos ;; usage: (subscribe [:todos])
|
||||
(fn [db _]
|
||||
(reaction (vals (:todos @db)))))
|
||||
|
||||
(register-sub
|
||||
;; So here we define the handler for another intermediate node.
|
||||
;; This time the computation involves two input signals.
|
||||
;; As a result note:
|
||||
;; - the first function (which returns the signals, returns a 2-vector)
|
||||
;; - the second function (which is the computation, destructures this 2-vector as its first parameter)
|
||||
(register-pure-sub
|
||||
:visible-todos
|
||||
(fn [db _]
|
||||
(reaction (let [filter-fn (filter-fn-for (:showing @db))
|
||||
todos (vals (:todos @db))]
|
||||
(filter filter-fn todos)))))
|
||||
(fn [query-v _] ;; returns a vector of two signals.
|
||||
[(subscribe [:todos])
|
||||
(subscribe [:showing])])
|
||||
|
||||
(register-sub
|
||||
(fn [[todos showing] _] ;; that 1st parameter is a 2-vector of values
|
||||
(let [filter-fn (case showing
|
||||
:active (complement :done)
|
||||
:done :done
|
||||
:all identity)]
|
||||
(filter filter-fn todos))))
|
||||
|
||||
;; -------------------------------------------------------------------------------------
|
||||
;; HEY, WAIT ON
|
||||
;;
|
||||
;; How did those two simple registrations at the top work, I hear you ask?
|
||||
;; We supplied only one function in those registrations, not two?
|
||||
;; I'm glad you asked.
|
||||
;; You see, when the signal-returning-fn is omitted, register-pure-sub provides a default.
|
||||
;; And it loks like this:
|
||||
;; (fn [_ _] re-frame.db/app-db)
|
||||
;; You can see that it returns one signal, and that signal is app-db itself.
|
||||
;;
|
||||
;; So that's why those two simple registrations didn't provide a signal-fn, but they
|
||||
;; still got the value in app-db supplied as that first parameter.
|
||||
|
||||
;; -------------------------------------------------------------------------------------
|
||||
;; SUGAR ?
|
||||
;; Now for some syntactic sugar...
|
||||
;; The purpose of the sugar is to remove boilerplate noise. To distill to the essential
|
||||
;; in 90% of cases.
|
||||
;; Is this a good idea?
|
||||
;; If it is a good idea, is this good syntax?
|
||||
;; Because it is so common to nominate 1 or more input signals,
|
||||
;; register-pure-sub provides some macro sugar so you can nominate a very minimal
|
||||
;; vector of input signals. The 1st function is not needed.
|
||||
;; Here is the example above rewritten using the sugar.
|
||||
#_(register-pure-sub
|
||||
:visible-todos
|
||||
:<- [:todos]
|
||||
:<- [:showing]
|
||||
(fn [[todos showing] _]
|
||||
(let [filter-fn (case showing
|
||||
:active (complement :done)
|
||||
:done :done
|
||||
:all identity)]
|
||||
(filter filter-fn todos))))
|
||||
|
||||
|
||||
(register-pure-sub
|
||||
:all-complete?
|
||||
:<- [:todos]
|
||||
(fn [[todos] _]
|
||||
(seq todos)))
|
||||
|
||||
(register-pure-sub
|
||||
:completed-count
|
||||
(fn [db _]
|
||||
(reaction (completed-count (:todos @db)))))
|
||||
|
||||
(register-sub
|
||||
:footer-stats
|
||||
(fn [db _]
|
||||
(reaction
|
||||
(let [todos (:todos @db)
|
||||
completed-count (completed-count todos)
|
||||
active-count (- (count todos) completed-count)
|
||||
showing (:showing @db)]
|
||||
[active-count completed-count showing])))) ;; tuple
|
||||
:<- [:todos]
|
||||
(fn [[todos] _]
|
||||
(count (filter :done todos))))
|
||||
|
||||
(register-pure-sub
|
||||
:footer-counts ;; XXXX different from original. Now does not return showing
|
||||
:<- [:todos]
|
||||
:<- [:completed-count]
|
||||
(fn [[todos completed] _]
|
||||
[(- (count todos) completed) completed]))
|
||||
|
|
|
@ -1,19 +1,20 @@
|
|||
(ns todomvc.views
|
||||
(:require [reagent.core :as reagent :refer [atom]]
|
||||
(:require [reagent.core :as reagent]
|
||||
[re-frame.core :refer [subscribe dispatch]]))
|
||||
|
||||
|
||||
(defn todo-input [{:keys [title on-save on-stop]}]
|
||||
(let [val (atom title)
|
||||
(let [val (reagent/atom title)
|
||||
stop #(do (reset! val "")
|
||||
(if on-stop (on-stop)))
|
||||
(when on-stop (on-stop)))
|
||||
save #(let [v (-> @val str clojure.string/trim)]
|
||||
(if-not (empty? v) (on-save v))
|
||||
(when (seq v) (on-save v))
|
||||
(stop))]
|
||||
(fn [props]
|
||||
[:input (merge props
|
||||
{:type "text"
|
||||
:value @val
|
||||
:auto-focus true
|
||||
:on-blur save
|
||||
:on-change #(reset! val (-> % .-target .-value))
|
||||
:on-key-down #(case (.-which %)
|
||||
|
@ -21,76 +22,95 @@
|
|||
27 (stop)
|
||||
nil)})])))
|
||||
|
||||
(def todo-edit (with-meta todo-input
|
||||
{:component-did-mount #(.focus (reagent/dom-node %))}))
|
||||
|
||||
(defn stats-footer
|
||||
[]
|
||||
(let [footer-stats (subscribe [:footer-stats])]
|
||||
(fn []
|
||||
(let [[active done filter] @footer-stats
|
||||
props-for (fn [filter-kw txt]
|
||||
[:a {:class (if (= filter-kw filter) "selected")
|
||||
:href (str "#/" (name filter-kw))} txt])]
|
||||
[:footer.footer
|
||||
[:div
|
||||
[:span.todo-count
|
||||
[:strong active] " " (case active 1 "item" "items") " left"]
|
||||
[:ul.filters
|
||||
[:li (props-for :all "All")]
|
||||
[:li (props-for :active "Active")]
|
||||
[:li (props-for :done "Completed")]]
|
||||
(when (pos? done)
|
||||
[:button.clear-completed {:on-click #(dispatch [:clear-completed])}
|
||||
"Clear completed"])]]))))
|
||||
|
||||
(defn todo-item
|
||||
[]
|
||||
(let [editing (atom false)]
|
||||
(let [editing (reagent/atom false)]
|
||||
(fn [{:keys [id done title]}]
|
||||
[:li {:class (str (if done "completed ")
|
||||
(if @editing "editing"))}
|
||||
[:div.view
|
||||
[:input.toggle {:type "checkbox"
|
||||
:checked done
|
||||
:on-change #(dispatch [:toggle-done id])}]
|
||||
[:label {:on-double-click #(reset! editing true)} title]
|
||||
[:button.destroy {:on-click #(dispatch [:delete-todo id])}]]
|
||||
[:div.view
|
||||
[:input.toggle
|
||||
{:type "checkbox"
|
||||
:checked done
|
||||
:on-change #(dispatch [:toggle-done id])}]
|
||||
[:label
|
||||
{:on-double-click #(reset! editing true)}
|
||||
title]
|
||||
[:button.destroy
|
||||
{:on-click #(dispatch [:delete-todo id])}]]
|
||||
(when @editing
|
||||
[todo-edit {:class "edit"
|
||||
:title title
|
||||
:on-save #(dispatch [:save id %])
|
||||
:on-stop #(reset! editing false)}])])))
|
||||
[todo-input
|
||||
{:class "edit"
|
||||
:title title
|
||||
:on-save #(dispatch [:save id %])
|
||||
:on-stop #(reset! editing false)}])])))
|
||||
|
||||
|
||||
(defn footer
|
||||
[]
|
||||
[:footer#info
|
||||
[:p "Double-click to edit a todo"]])
|
||||
|
||||
|
||||
(defn task-list
|
||||
[]
|
||||
(let [visible-todos (subscribe [:visible-todos])
|
||||
all-complete? (subscribe [:all-complete?])]
|
||||
(fn []
|
||||
[:section#main
|
||||
[:input#toggle-all
|
||||
{:type "checkbox"
|
||||
:checked @all-complete?
|
||||
:on-change #(dispatch [:complete-all-toggle (not @all-complete?)])}]
|
||||
[:label
|
||||
{:for "toggle-all"}
|
||||
"Mark all as complete"]
|
||||
[:ul#todo-list
|
||||
(for [todo @visible-todos]
|
||||
^{:key (:id todo)} [todo-item todo])]])))
|
||||
|
||||
|
||||
(defn footer-controls
|
||||
[]
|
||||
(let [footer-stats (subscribe [:footer-counts])
|
||||
showing (subscribe [:showing])]
|
||||
(fn []
|
||||
(let [[active done] @footer-stats
|
||||
a-fn (fn [filter-kw txt]
|
||||
[:a {:class (when (= filter-kw @showing) "selected")
|
||||
:href (str "#/" (name filter-kw))} txt])]
|
||||
[:footer#footer
|
||||
|
||||
[:span#todo-count
|
||||
[:strong active] " " (case active 1 "item" "items") " left"]
|
||||
[:ul#filters
|
||||
[:li (a-fn :all "All")]
|
||||
[:li (a-fn :active "Active")]
|
||||
[:li (a-fn :done "Completed")]]
|
||||
(when (pos? done)
|
||||
[:button#clear-completed {:on-click #(dispatch [:clear-completed])}
|
||||
"Clear completed"])]))))
|
||||
|
||||
|
||||
(defn task-entry
|
||||
[]
|
||||
[:header#header
|
||||
[:h1 "todos"]
|
||||
[todo-input
|
||||
{:id "new-todo"
|
||||
:placeholder "What needs to be done?"
|
||||
:on-save #(dispatch [:add-todo %])}]])
|
||||
|
||||
(defn todo-list
|
||||
[visible-todos]
|
||||
[:ul.todo-list
|
||||
(for [todo @visible-todos]
|
||||
^{:key (:id todo)} [todo-item todo])])
|
||||
|
||||
(defn todo-app
|
||||
[]
|
||||
(let [todos (subscribe [:todos])
|
||||
visible-todos (subscribe [:visible-todos])
|
||||
completed-count (subscribe [:completed-count])]
|
||||
(let [todos (subscribe [:todos])]
|
||||
(fn []
|
||||
[:div
|
||||
[:section.todoapp
|
||||
[:header#header
|
||||
[:h1 "todos"]
|
||||
[todo-input {:class "new-todo"
|
||||
:placeholder "What needs to be done?"
|
||||
:on-save #(dispatch [:add-todo %])}]]
|
||||
(when-not (empty? @todos)
|
||||
[:div
|
||||
[:section.main
|
||||
[:input.toggle-all
|
||||
{:type "checkbox"
|
||||
:checked (pos? @completed-count)
|
||||
:on-change #(dispatch [:complete-all-toggle])}]
|
||||
[:label {:for "toggle-all"} "Mark all as complete"]
|
||||
[todo-list visible-todos]]
|
||||
[stats-footer]])]
|
||||
[:footer.info
|
||||
[:p "Double-click to edit a todo"]]])))
|
||||
|
||||
[:section#todoapp
|
||||
[task-entry]
|
||||
(when (seq @todos)
|
||||
[task-list])
|
||||
[footer-controls]]
|
||||
[footer]])))
|
||||
|
|
|
@ -7,7 +7,8 @@
|
|||
[reagent "0.6.0-rc"]]
|
||||
|
||||
:profiles {:debug {:debug true}
|
||||
:dev {:dependencies [[karma-reporter "0.3.0"]]
|
||||
:dev {:dependencies [[karma-reporter "0.3.0"]
|
||||
[binaryage/devtools "0.7.0"]]
|
||||
:plugins [[lein-cljsbuild "1.1.3"]
|
||||
[lein-npm "0.6.1"]
|
||||
[lein-figwheel "0.5.4-2"]]}}
|
||||
|
@ -33,7 +34,7 @@
|
|||
:compiler {:output-to "run/compiled/test.js"
|
||||
:source-map "run/compiled/test.js.map"
|
||||
:output-dir "run/compiled/test"
|
||||
:optimizations :simple
|
||||
:optimizations :whitespace
|
||||
:pretty-print true}}]}
|
||||
|
||||
:aliases {"auto" ["do" "clean," "cljsbuild" "auto" "test,"]
|
||||
|
|
|
@ -11,23 +11,23 @@
|
|||
;; -- Configuration ----------------------------------------------------------
|
||||
;;
|
||||
;;
|
||||
(def ^:private undo-config (atom {:max-undos 50} ;; Maximum number of undo states maintained
|
||||
{:path []})) ;; undo and redos will apply to only this path within app-db
|
||||
(def ^:private config (atom {:max-undos 50 ;; Maximum number of undo states maintained
|
||||
:path []})) ;; undo and redos will apply to only this path within app-db
|
||||
(defn set-max-undos!
|
||||
[n]
|
||||
(swap! undo-config assoc :max-undos n))
|
||||
(swap! config assoc :max-undos n))
|
||||
|
||||
(defn set-undo-path!
|
||||
[ks]
|
||||
(swap! undo-config assoc :path ks))
|
||||
(swap! config assoc :path ks))
|
||||
|
||||
(defn max-undos
|
||||
[]
|
||||
(:max-undos @undo-config))
|
||||
(:max-undos @config))
|
||||
|
||||
(defn undo-path-ks
|
||||
[]
|
||||
(:path @undo-config))
|
||||
(:path @config))
|
||||
|
||||
|
||||
;; -- State history ----------------------------------------------------------
|
||||
|
@ -132,6 +132,11 @@
|
|||
[undos cur redos]
|
||||
(let [u @undos
|
||||
r (cons @cur @redos)]
|
||||
|
||||
(.log js/console "in undo")
|
||||
(.log js/console cur)
|
||||
(.log js/console (undo-path-ks))
|
||||
(.log js/console (last u))
|
||||
(swap! cur assoc-in (undo-path-ks) (last u))
|
||||
(reset! redos r)
|
||||
(reset! undos (pop u))))
|
||||
|
@ -148,6 +153,7 @@
|
|||
:undo ;; usage: (dispatch [:undo n]) n is optional, defaults to 1
|
||||
(fn handler
|
||||
[_ [_ n]]
|
||||
(.log js/console "in :undo handler")
|
||||
(if-not (undos?)
|
||||
(warn "re-frame: you did a (dispatch [:undo]), but there is nothing to undo.")
|
||||
(undo-n (or n 1)))))
|
||||
|
@ -210,3 +216,35 @@
|
|||
(handler db event-vec)))))
|
||||
|
||||
(def undoable (with-meta undoable_ {:re-frame-factory-name "undoable"}))
|
||||
|
||||
|
||||
|
||||
;; middleware
|
||||
(defn fsm-trigger
|
||||
[trigger update-fn fsm-path]
|
||||
(fn fsm-middleware
|
||||
[handler]
|
||||
(fn fsm-handler
|
||||
[db event-v]
|
||||
(let [new-db (handler db event-v)]
|
||||
(update-fn new-db event-v trigger fsm-path)))) ;; think about access to event-v
|
||||
|
||||
|
||||
(register-handler
|
||||
:database-connection
|
||||
(fsm-tirgger :db-working bootstrap-fsm [:fsms :bnootstrap] )
|
||||
(fn [db q]
|
||||
.....)
|
||||
)
|
||||
|
||||
|
||||
;; state
|
||||
;; fms-state
|
||||
;; seen-evetns
|
||||
;; started-tasks
|
||||
|
||||
(defn bootstrap-fsm
|
||||
[new-db event-v trigger fsm-path]
|
||||
(let [new-state ]
|
||||
)
|
||||
)
|
||||
|
|
|
@ -2,8 +2,10 @@
|
|||
(:require [jx.reporter.karma :as karma :include-macros true]
|
||||
[re-frame.test.middleware]
|
||||
[re-frame.test.undo]
|
||||
[re-frame.test.subs]))
|
||||
[re-frame.test.subs]
|
||||
[devtools.core :as devtools]))
|
||||
|
||||
(devtools/install!) ;; we love https://github.com/binaryage/cljs-devtools
|
||||
|
||||
(defn ^:export run [karma]
|
||||
(karma/run-tests
|
||||
|
|
|
@ -4,40 +4,50 @@
|
|||
[re-frame.db :as db]
|
||||
[re-frame.core :as re-frame]))
|
||||
|
||||
|
||||
|
||||
(deftest test-undos
|
||||
;; Create undo history
|
||||
(undo/set-max-undos! 5)
|
||||
|
||||
(undo/clear-history!)
|
||||
(is (not (undo/undos?)))
|
||||
(is (not (undo/redos?)))
|
||||
|
||||
(doseq [i (range 10)]
|
||||
(reset! db/app-db i)
|
||||
(reset! db/app-db {i i})
|
||||
(undo/store-now! i))
|
||||
|
||||
;; Check the undo state is correct
|
||||
(is (undo/undos?))
|
||||
(is (not (undo/redos?)))
|
||||
(is (= [4 5 6 7 8 9] (undo/undo-explanations)))
|
||||
(is (= [5 6 7 8 9] @undo/undo-list))
|
||||
(is (= [{5 5} {6 6} {7 7} {8 8} {9 9}] @undo/undo-list))
|
||||
|
||||
(.log js/console "----1-----")
|
||||
;; Undo the actions
|
||||
(re-frame/dispatch-sync [:undo])
|
||||
(is (= @db/app-db 9))
|
||||
(.log js/console "----2-----")
|
||||
(is (= @db/app-db {9 9}))
|
||||
(is (undo/redos?))
|
||||
(re-frame/dispatch-sync [:undo])
|
||||
(is (= @db/app-db 8))
|
||||
(is (= @db/app-db {8 8}))
|
||||
(re-frame/dispatch-sync [:undo])
|
||||
(is (= @db/app-db 7))
|
||||
(is (= @db/app-db {7 7}))
|
||||
(re-frame/dispatch-sync [:undo])
|
||||
(is (= @db/app-db 6))
|
||||
(is (= @db/app-db {6 6}))
|
||||
(re-frame/dispatch-sync [:undo])
|
||||
(is (= @db/app-db 5))
|
||||
(is (= @db/app-db {5 5}))
|
||||
(is (not (undo/undos?)))
|
||||
(is (undo/redos?))
|
||||
|
||||
|
||||
;; Redo them again
|
||||
(re-frame/dispatch-sync [:redo 5])
|
||||
(is (= @db/app-db 9))
|
||||
(is (= @db/app-db {9 9}))
|
||||
(is (not (undo/redos?)))
|
||||
(is (undo/undos?))
|
||||
(is (= [5 6 7 8 9] @undo/undo-list))
|
||||
(is (= [{5 5} {6 6} {7 7} {8 8} {9 9}] @undo/undo-list))
|
||||
|
||||
;; Clear history
|
||||
(undo/clear-history!)
|
||||
|
|
Loading…
Reference in New Issue