Skip to content

Instantly share code, notes, and snippets.

@Janiczek
Last active January 2, 2016 06:09
Show Gist options
  • Save Janiczek/8261582 to your computer and use it in GitHub Desktop.
Save Janiczek/8261582 to your computer and use it in GitHub Desktop.
TodoMVC in Om. See file info.md.

TodoMVC in Om + Sablono + core.async (well, core.async not so much in the end)

This is a result of a three-ish hours long try to understand Om better on a TodoMVC example. Next steps would be finishing implementing rest of the functionality as dictated by the app specification, and then refactoring. And then some more refactoring. And then ...

I recorded a screencast of this: http://youtu.be/7j133D79Vaw

(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))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment