Skip to content

Instantly share code, notes, and snippets.

Last active August 6, 2023 03:30
  • Star 9 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
What would you like to do?
Photon TodoMVC, with a twist!

TodoMVC, with a twist! — Electric Clojure

  1. it's multiplayer! 0 LOC cost
  2. state is durable! (server side database) 0 LOC cost
  3. See those pending spinners? We've added a server delay to demonstrate managed load states.

Electric is a "multi tier" Clojure/Script dialect for full-stack web application development. It uses macros to let you interweave client and server code in a single .CLJC file, so you can define a full-stack frontend/backend webapp all in one place. Electric is designed for rich dynamic applications with reactive user interfaces and complex frontend/backend data sync requirements.

C-f for "e/server" in the below gist to understand the client/server boundary

(ns user.demo-5-todomvc
#?(:cljs (:require-macros user.demo-5-todomvc))
#?(:clj [datascript.core :as d])
[hyperfiddle.electric :as e]
[hyperfiddle.electric-dom2 :as dom]
[hyperfiddle.electric-ui4 :as ui]))
(defonce !conn #?(:clj (d/create-conn {}) :cljs nil)) ; server
(e/def db) ; server
(e/def transact!) ; server
(def !state #?(:cljs (atom {::filter :all ; client
::editing nil
::delay 0})))
(defn query-todos [db filter]
{:pre [filter]}
(case filter
:active (d/q '[:find [?e ...] :where [?e :task/status :active]] db)
:done (d/q '[:find [?e ...] :where [?e :task/status :done]] db)
:all (d/q '[:find [?e ...] :where [?e :task/status]] db))))
(defn todo-count [db filter]
{:pre [filter]
:post [(number? %)]}
(-> (case filter
:active (d/q '[:find (count ?e) . :where [?e :task/status :active]] db)
:done (d/q '[:find (count ?e) . :where [?e :task/status :done]] db)
:all (d/q '[:find (count ?e) . :where [?e :task/status]] db))
(or 0)))) ; datascript can return nil wtf
(e/defn Filter-control [state target label]
(dom/a (dom/props {:class (when (= state target) "selected")})
(dom/text label)
(dom/on "click" (e/fn [_] (swap! !state assoc ::filter target)))))
(e/defn TodoStats [state]
(let [active (e/server (todo-count db :active))
done (e/server (todo-count db :done))]
(dom/span (dom/props {:class "todo-count"})
(dom/strong (dom/text active))
(dom/span (dom/text " " (str (case active 1 "item" "items")) " left")))
(dom/ul (dom/props {:class "filters"})
(dom/li (Filter-control. (::filter state) :all "All"))
(dom/li (Filter-control. (::filter state) :active "Active"))
(dom/li (Filter-control. (::filter state) :done "Completed")))
(when (pos? done)
(ui/button (e/fn [] (e/server (when-some [ids (seq (query-todos db :done))]
(transact! (mapv (fn [id] [:db/retractEntity id]) ids)) nil)))
(dom/props {:class "clear-completed"})
(dom/text "Clear completed " done))))))
(e/defn TodoItem [state id]
(let [{:keys [:task/status :task/description]} (d/entity db id)]
(dom/props {:class [(when (= :done status) "completed")
(when (= id (::editing state)) "editing")]})
(dom/div (dom/props {:class "view"})
(ui/checkbox (= :done status) (e/fn [v]
(let [status (case v true :done, false :active, nil)]
(e/server (transact! [{:db/id id, :task/status status}]) nil)))
(dom/props {:class "toggle"}))
(dom/label (dom/text description)
(dom/on "dblclick" (e/fn [_] (swap! !state assoc ::editing id)))))
(when (= id (::editing state))
(dom/span (dom/props {:class "input-load-mask"})
(dom/on-pending (dom/props {:aria-busy true})
(dom/on "keydown"
(e/fn [e]
(case (.-key e)
"Enter" (when-some [description (contrib.str/blank->nil (-> e .-target .-value))]
(case (e/server (transact! [{:db/id id, :task/description description}]) nil)
(swap! !state assoc ::editing nil)))
"Escape" (swap! !state assoc ::editing nil)
(dom/props {:class "edit" #_#_:autofocus true})
(dom/bind-value description) ; first set the initial value, then focus
(case description ; HACK sequence - run focus after description is available
(.focus dom/node))))))
(ui/button (e/fn [] (e/server (transact! [[:db/retractEntity id]]) nil))
(dom/props {:class "destroy"})))))))
(defn toggle-all! [db status]
(let [ids (query-todos db (if (= :done status) :active :done))]
(map (fn [id] {:db/id id, :task/status status}) ids))))
(e/defn TodoList [state]
(dom/section (dom/props {:class "main"})
(let [active (e/server (todo-count db :active))
all (e/server (todo-count db :all))
done (e/server (todo-count db :done))]
(ui/checkbox (cond (= all done) true
(= all active) false
:else nil)
(e/fn [v] (let [status (case v (true nil) :done, false :active)]
(e/server (transact! (toggle-all! db status)) nil)))
(dom/props {:class "toggle-all"})))
(dom/label (dom/props {:for "toggle-all"}) (dom/text "Mark all as complete"))
(dom/ul (dom/props {:class "todo-list"})
(e/for [id (e/server (sort (query-todos db (::filter state))))]
(TodoItem. state id)))))))
(e/defn CreateTodo []
(dom/span (dom/props {:class "input-load-mask"})
(dom/on-pending (dom/props {:aria-busy true})
(dom/on "keydown"
(e/fn [e]
(when (= "Enter" (.-key e))
(when-some [description (contrib.str/empty->nil (-> e .-target .-value))]
(e/server (transact! [{:task/description description, :task/status :active}]) nil)
(set! (.-value dom/node) "")))))
(dom/props {:class "new-todo", :placeholder "What needs to be done?"})))))
(e/defn TodoApp [state]
(dom/section (dom/props {:class "todoapp"})
(dom/header (dom/props {:class "header"})
(when (e/server (pos? (todo-count db :all)))
(TodoList. state))
(dom/footer (dom/props {:class "footer"})
(TodoStats. state))))
(e/defn TodoMVC [state]
(dom/div (dom/props {:class "todomvc"})
(dom/h1 (dom/text "TodoMVC"))
(TodoApp. state)
(dom/footer (dom/props {:class "info"})
(dom/p (dom/text "Double-click to edit a todo")))))
(defn slow-transact! [!conn delay tx]
(try (Thread/sleep delay) ; artificial latency
(d/transact! !conn tx)
(catch InterruptedException _))))
(e/defn App []
(let [state (e/watch !state)]
(binding [db (e/watch !conn)
transact! (partial slow-transact! !conn (e/client (::delay state)))]
(dom/link (dom/props {:rel :stylesheet, :href "/todomvc.css"}))
(dom/element "style" (dom/text "body { width: 65vw; margin-left: auto; margin-right: auto; }"))
(TodoMVC. state)))))))
Copy link

dustingetz commented Aug 25, 2022

Copy link


Def going to try this out.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment