|
(ns todomvc.core |
|
(:require |
|
[om.core :as om :include-macros true] |
|
[sablono.core :as html :refer [html] :include-macros true] |
|
[cljs.core.async :refer [<! chan timeout]]) |
|
(:require-macros |
|
[cljs.core.async.macros :refer [go]])) |
|
|
|
; -------------------------- |
|
; HELPERS |
|
|
|
(def ENTER_KEY 13) |
|
(def ESCAPE_KEY 27) |
|
|
|
(defn completed-count [todos] |
|
(count (filter #(true? (:completed? %)) |
|
todos))) |
|
|
|
(defn todo-count [todos] |
|
(- (count todos) |
|
(completed-count todos))) |
|
|
|
(defn pluralize [noun count] |
|
(case count |
|
1 noun |
|
(str noun "s"))) |
|
|
|
(defn toggle-all [todos yesno] |
|
(mapv #(assoc % :completed? yesno) todos)) |
|
|
|
(defn toggle-one [todo] |
|
(update-in todo [:completed?] not)) |
|
|
|
; -------------------------- |
|
; STATE |
|
|
|
(def state (atom [])) |
|
|
|
; -------------------------- |
|
; ASYNC |
|
|
|
(defn handle-keydown [owner e] |
|
(if (= ENTER_KEY (.-which e)) |
|
(let [val (-> e .-target .-value .trim)] |
|
(when (not-empty val) |
|
(swap! state conj {:content val |
|
:temp-content val |
|
:editing? false |
|
:completed? false}) |
|
(set! (.-value (om/get-node owner "new-todo")) ""))))) |
|
|
|
(defn handle-toggle-all [owner e] |
|
(let [checked? (-> e .-target .-checked)] |
|
(swap! state toggle-all checked?))) |
|
|
|
(defn handle-toggle-one [todo] |
|
(om/update! todo toggle-one)) |
|
|
|
(defn handle-delete [todo todos] |
|
(let [todo (dissoc (.-value todo) :editing?)] |
|
(om/update! todos #(into [] (filter (fn [t] |
|
(not= (dissoc t :editing?) |
|
todo)) %))))) |
|
|
|
(defn handle-clear-completed [todos] |
|
(om/update! todos #(into [] (filter (fn [t] (not (:completed? t))) %)))) |
|
|
|
(defn handle-start-editing [todo owner] |
|
(let [node (om/get-node owner "edit") |
|
len (.. node -value -length)] |
|
(.focus node) |
|
(.setSelectionRange node len len)) |
|
(om/update! todo #(assoc % :editing? true))) |
|
|
|
; TODO: how to make it forget the temporary value? |
|
; for now it stays in the input ... |
|
; probably something with react ... |
|
(defn handle-abort-editing [todo] |
|
(om/update! todo #(assoc % :editing? false)) |
|
(om/update! todo #(assoc % :temp-content (om/read todo :content)))) |
|
|
|
(defn handle-stop-editing [todo todos] |
|
(when (om/read todo :editing?) |
|
(om/update! todo #(assoc % :editing? false)) |
|
(let [val (.trim (om/read todo :temp-content))] |
|
(if (empty? val) |
|
(handle-delete todo todos) |
|
(do |
|
(om/update! todo #(assoc % :temp-content (om/read todo :content))) |
|
(om/update! todo #(assoc % :content val))))))) |
|
|
|
(defn handle-edit-keydown [todo todos e] |
|
(let [key (.-which e)] |
|
(condp = key |
|
ENTER_KEY (handle-stop-editing todo todos) |
|
ESCAPE_KEY (handle-abort-editing todo) |
|
nil))) |
|
|
|
(defn handle-temp-change [todo e] |
|
(om/update! todo #(assoc % :temp-content (-> e .-target .-value)))) |
|
|
|
; TODO routing - NOT TODAY :) |
|
|
|
; -------------------------- |
|
; COMPONENTS |
|
|
|
(defn header [app owner] |
|
(om/component |
|
(html |
|
[:header {:id "header"} |
|
[:h1 "todos"] |
|
[:input {:id "new-todo" |
|
:ref "new-todo" |
|
:placeholder "What needs to be done?" |
|
:autoFocus true |
|
:onKeyDown #(handle-keydown owner %)}]]))) |
|
|
|
(defn todo-item [{:keys [content completed? editing?] |
|
:or {:content "" |
|
:completed? false |
|
:editing? false} |
|
:as todo} |
|
owner |
|
{app :app}] |
|
(let [temp-content (get todo :temp-content (get todo :content))] |
|
(om/component |
|
(html |
|
[:li {:className (apply str |
|
(interpose " " |
|
(filter #(not (nil? %)) |
|
[(when editing? "editing") |
|
(when completed? "completed")])))} |
|
[:div {:className "view"} |
|
[:input {:className "toggle" |
|
:type "checkbox" |
|
:onClick #(handle-toggle-one todo) |
|
:checked completed?}] |
|
[:label {:onDoubleClick #(handle-start-editing todo owner)} |
|
content] |
|
[:button {:className "destroy" |
|
:onClick #(handle-delete todo app)}]] |
|
[:input {:className "edit" |
|
:ref "edit" |
|
:defaultValue temp-content |
|
:onChange #(handle-temp-change todo %) |
|
:onKeyDown #(handle-edit-keydown todo app %) |
|
:onBlur #(handle-stop-editing todo app)}]])))) |
|
|
|
(defn main [app owner] |
|
(om/component |
|
(html |
|
(if (zero? (count app)) |
|
[:div {:style {:display "none"}}] |
|
[:section {:id "main"} |
|
[:input {:id "toggle-all" |
|
:type "checkbox" |
|
:ref "toggle-all" |
|
:checked (= (completed-count app) (count app)) |
|
:onClick #(handle-toggle-all owner %)}] |
|
[:label {:htmlFor "toggle-all"} |
|
"Mark all as complete"] |
|
[:ul {:id "todo-list"} |
|
(om/build-all todo-item app {:opts {:app app}})]])))) |
|
|
|
(defn footer [app] |
|
(let [completed-count (completed-count app) |
|
todo-count (todo-count app)] |
|
(om/component |
|
(html |
|
(if (zero? (count app)) |
|
[:div {:style {:display "none"}}] |
|
[:footer {:id "footer"} |
|
[:span {:id "todo-count"} |
|
[:strong todo-count] |
|
(str " " (pluralize "item" todo-count) " left")] |
|
; [:ul {:id "filters"} |
|
; [:li |
|
; [:a {:className "selected" |
|
; :href "#/"} |
|
; "All"]] |
|
; [:li |
|
; [:a {:href "#/active"} |
|
; "Active"]] |
|
; [:li |
|
; [:a {:href "#/completed"} |
|
; "Completed"]]] |
|
[:button (merge {:id "clear-completed" |
|
:onClick #(handle-clear-completed app)} |
|
(when (zero? completed-count) |
|
{:style {:display "none"}})) |
|
(str "Clear completed (" completed-count ")")]]))))) |
|
|
|
(defn info [] |
|
(om/component |
|
(html |
|
[:footer {:id "info"} |
|
[:p "Double-click to edit a todo"] |
|
[:p "Template by " |
|
[:a {:href "http://github.com/sindresorhus"} |
|
"Sindre Sorhus"]] |
|
[:p "Created by " |
|
[:a {:href "http://martin.janiczek.cz"} |
|
"Martin Janiczek"]] |
|
[:p "Part of " |
|
[:a {:href "http://todomvc.com"} |
|
"TodoMVC"]]]))) |
|
|
|
(defn todoapp [app] |
|
(om/component |
|
(html |
|
[:div {:id "page"} |
|
[:section {:id "todoapp"} |
|
(om/build header app) |
|
(om/build main app) |
|
(om/build footer app)] |
|
(om/build info app)]))) |
|
|
|
; -------------------------- |
|
; GO! |
|
|
|
(om/root state |
|
todoapp |
|
(.-body js/document)) |