update/fix todomvc example

This commit is contained in:
Gered 2022-01-12 17:18:36 -05:00
parent 17667c274a
commit 414c491483
4 changed files with 106 additions and 86 deletions

View file

@ -1,9 +1,8 @@
# views.reagent Example - Todo MVC
This is a modification of the Todo MVC app for Reagent [demonstrated here][1].
This version of the app has been modified to use a PostgreSQL database
to store the Todos and to provide realtime synchronization of changes
to that data to any number of users currently viewing the app.
This is a modification of the Todo MVC app for Reagent [demonstrated here][1]. This version of the
app has been modified to use a PostgreSQL database to store the Todos and to provide realtime
synchronization of changes to that data to any number of users currently viewing the app.
[1]: http://reagent-project.github.io/
@ -11,9 +10,16 @@ to that data to any number of users currently viewing the app.
### Creating the Database
This example app uses a PostgreSQL database. The SQL script to create
it is in `create_db.sql`. You can easily pipe it into `psql` at a
command line to create it quickly, for example:
This example app uses a PostgreSQL database. The SQL script to create it is in `create_db.sql`.
A Docker compose file `pgsql.docker-compose.yml` is provided and pre-configured to allow you to
quickly spin up a PostgreSQL database that will be pre-initialized via `create_db.sql` through
Docker.
$ docker-compose -f pgsql.docker-compose.yml up
Alternatively, if you already have a PostgreSQL database available, you can run the
`create_db.sql` via `psql` easily enough:
$ psql < create_db.sql
@ -25,11 +31,9 @@ To build everything and run in one step:
$ lein rundemo
Then open up a web browser or two and head to http://localhost:8080/
to see the web app in action.
Then open up a web browser or two and head to http://localhost:8080/ to see the web app in action.
If you want to run this application in a REPL, just be sure to build
the ClojureScript:
If you want to run this application in a REPL, just be sure to build the ClojureScript:
$ lein cljsbuild once
@ -37,5 +41,4 @@ And then in the REPL you can just run:
(-main)
to start the web app (you should be put in the correct namespace
immediately).
to start the web app (you should be put in the correct namespace immediately).

View file

@ -1,27 +1,26 @@
(defproject todomvc "0.1.0-SNAPSHOT"
:dependencies [[org.clojure/clojure "1.8.0"]
[org.clojure/clojurescript "1.8.51"]
[ring "1.4.0"]
[ring/ring-defaults "0.2.0" :exclusions [javax.servlet/servlet-api]]
[compojure "1.4.0"]
[org.immutant/web "2.1.4"]
:dependencies [[org.clojure/clojure "1.10.3"]
[org.clojure/clojurescript "1.10.773"]
[ring "1.9.4"]
[ring/ring-defaults "0.3.3" :exclusions [javax.servlet/servlet-api]]
[compojure "1.6.2"]
[org.immutant/web "2.1.10"]
[org.clojure/java.jdbc "0.6.1"]
[org.postgresql/postgresql "9.4.1208.jre7"]
[com.taoensso/sente "1.8.1"]
[gered/views "1.5"]
[gered/views.sql "0.1"]
[gered/views.reagent "0.1"]
[gered/views.reagent.sente "0.1"]
[org.clojure/java.jdbc "0.7.12"]
[org.postgresql/postgresql "42.3.1"]
[com.taoensso/sente "1.16.2"]
[net.gered/views "1.6-SNAPSHOT"]
[net.gered/views.sql "0.2-SNAPSHOT"]
[net.gered/views.reagent "0.2-SNAPSHOT"]
[net.gered/views.reagent.sente "0.2-SNAPSHOT"]
[hiccup "1.0.5"]
[reagent "0.6.0-alpha2"]
[cljs-ajax "0.5.4"]
[reagent "1.1.0"]
[cljs-ajax "0.8.4"]
[cljsjs/react "17.0.2-0"]
[cljsjs/react-dom "17.0.2-0"]]
[environ "1.0.3"]]
:plugins [[lein-cljsbuild "1.1.3"]
[lein-environ "1.0.3"]]
:plugins [[lein-cljsbuild "1.1.8"]]
:main todomvc.server
@ -38,10 +37,9 @@
:optimizations :none
:pretty-print true}}}}
:profiles {:dev {:env {:dev "true"}}
:profiles {:dev {}
:uberjar {:env {}
:aot :all
:uberjar {:aot :all
:hooks [leiningen.cljsbuild]
:cljsbuild {:jar true
:builds {:main

View file

@ -1,7 +1,8 @@
(ns todomvc.client
(:require
[reagent.core :as r]
[ajax.core :refer [POST default-interceptors to-interceptor]]
[reagent.dom :as rdom]
[ajax.core :as ajax]
[taoensso.sente :as sente]
[views.reagent.client.component :refer [view-cursor] :refer-macros [defvc]]
[views.reagent.sente.client :as vr]))
@ -30,13 +31,13 @@
;; AJAX operations
(defn add-todo [text] (POST "/todos/add" {:format :url :params {:title text}}))
(defn toggle [id] (POST "/todos/toggle" {:format :url :params {:id id}}))
(defn save [id title] (POST "/todos/update" {:format :url :params {:id id :title title}}))
(defn delete [id] (POST "/todos/delete" {:format :url :params {:id id}}))
(defn add-todo [text] (ajax/POST "/todos/add" {:format :url :params {:title text}}))
(defn toggle [id] (ajax/POST "/todos/toggle" {:format :url :params {:id id}}))
(defn save [id title] (ajax/POST "/todos/update" {:format :url :params {:id id :title title}}))
(defn delete [id] (ajax/POST "/todos/delete" {:format :url :params {:id id}}))
(defn complete-all [v] (POST "/todos/mark-all" {:format :url :params {:done? v}}))
(defn clear-done [] (POST "/todos/delete-all-done"))
(defn complete-all [v] (ajax/POST "/todos/mark-all" {:format :url :params {:done? v}}))
(defn clear-done [] (ajax/POST "/todos/delete-all-done"))
@ -51,7 +52,7 @@
(if-not (empty? v) (on-save v))
(stop))]
(fn [props]
[:input (merge props
[:input (merge (dissoc props :on-save)
{:type "text" :value @val :on-blur save
:on-change #(reset! val (-> % .-target .-value))
:on-key-down #(case (.-which %)
@ -60,7 +61,7 @@
nil)})])))
(def todo-edit (with-meta todo-input
{:component-did-mount #(.focus (r/dom-node %))}))
{:component-did-mount #(.focus (rdom/dom-node %))}))
(defn todo-stats
[{:keys [filt active done]}]
@ -95,6 +96,24 @@
:on-stop #(reset! editing false)}])])))
(defn debug-component
[]
[:div
[:div "sente-socket" [:pre (pr-str (:state @sente-socket))]]
[:div "send-fn" [:pre (pr-str @views.reagent.client.core/send-fn)]]
[:div "view-data" [:pre (pr-str @views.reagent.client.core/view-data)]]
[:div [:button {:on-click (fn [e]
(println "sending event directly via sente-socket...")
((:send-fn @sente-socket) [:event/foo "foobar!"]))}
"send direct!"]]
[:div [:button {:on-click (fn [e]
(println "sending event via send-data! ...")
(views.reagent.client.core/send-data! [:event/foo "foobar!"]))}
"send via send-data!"]]
[:div [:button {:on-click (fn [e]
(println (:state @sente-socket)))}
"current state"]]])
;; Main TODO app component
;;
@ -155,22 +174,6 @@
;; Some unfortunately necessary set up to ensure we send the CSRF token back with
;; AJAX requests
(defn get-anti-forgery-token
[]
(if-let [hidden-field (.getElementById js/document "__anti-forgery-token")]
(.-value hidden-field)))
(def csrf-interceptor
(to-interceptor {:name "CSRF Interceptor"
:request #(assoc-in % [:headers "X-CSRF-Token"] (get-anti-forgery-token))}))
(swap! default-interceptors (partial cons csrf-interceptor))
;; Sente event/message handler
;;
;; Note that if you're only using Sente to make use of views.reagent in your app
@ -182,19 +185,33 @@
(defn sente-event-msg-handler
[{:keys [event id client-id] :as ev}]
(let [[ev-id ev-data] event]
(cond
(and (= :chsk/state ev-id)
(:open? ev-data))
(vr/on-open! @sente-socket ev)
(cond
(vr/chsk-open-event? ev)
(vr/on-open! @sente-socket ev)
(= :chsk/recv id)
(when-not (vr/on-receive! @sente-socket ev)
; on-receive! returns true if the event was a views.reagent event and it
; handled it.
;
; you could put your code to handle your app's own events here
))))
(= :chsk/recv id)
(when-not (vr/on-receive! @sente-socket ev)
; on-receive! returns true if the event was a views.reagent event and it
; handled it.
;
; you could put your code to handle your app's own events here
)))
;; Utility functions for dealing with CSRF Token garbage in AJAX requests.
(defn get-csrf-token
[]
(when-let [csrf-token-element (.querySelector js/document "meta[name=\"csrf-token\"]")]
(.-content csrf-token-element)))
(defn add-csrf-token-ajax-interceptor!
[csrf-token]
(let [interceptor (ajax/to-interceptor
{:name "CSRF Interceptor"
:request #(assoc-in % [:headers "X-CSRF-Token"] csrf-token)})]
(swap! ajax/default-interceptors #(cons interceptor %))))
@ -202,14 +219,19 @@
(defn ^:export run
[]
; Sente setup. create the socket, storing it in an atom and set up a event
; handler using sente's own message router functionality.
(reset! sente-socket (sente/make-channel-socket! "/chsk" {}))
(enable-console-print!)
; set up a handler for sente events
(sente/start-chsk-router! (:ch-recv @sente-socket) sente-event-msg-handler)
(let [csrf-token (get-csrf-token)]
(if csrf-token (add-csrf-token-ajax-interceptor! csrf-token))
; Configure views.reagent for use with Sente.
(vr/init! @sente-socket {})
; Sente setup. create the socket, storing it in an atom and set up a event
; handler using sente's own message router functionality.
(reset! sente-socket (sente/make-channel-socket! "/chsk" csrf-token {}))
(r/render-component [todo-app] (.getElementById js/document "app")))
; set up a handler for sente events
(sente/start-chsk-router! (:ch-recv @sente-socket) sente-event-msg-handler)
; Configure views.reagent for use with Sente.
(vr/init! @sente-socket {})
(rdom/render [todo-app] (.getElementById js/document "app"))))

View file

@ -3,22 +3,19 @@
(:require
[compojure.core :refer [routes GET POST]]
[compojure.route :as route]
[ring.middleware.anti-forgery :refer [*anti-forgery-token*]]
[ring.middleware.defaults :refer [wrap-defaults site-defaults]]
[ring.util.anti-forgery :refer [anti-forgery-field]]
[ring.util.response :refer [response]]
[taoensso.sente :as sente]
[taoensso.sente.server-adapters.immutant :refer [sente-web-server-adapter]]
[immutant.web :as immutant]
[hiccup.page :refer [html5 include-css include-js]]
[hiccup.element :refer [javascript-tag]]
[environ.core :refer [env]]
[clojure.java.jdbc :as jdbc]
[views.sql.core :refer [vexec! with-view-transaction]]
[views.sql.view :refer [view]]
[views.reagent.sente.server :as vr]))
(def dev? (boolean (env :dev)))
(def db {:classname "org.postgresql.Driver"
:subprotocol "postgresql"
:subname "//localhost/todomvc"
@ -133,11 +130,11 @@
[]
(html5
[:head
[:meta {:name "csrf-token" :content *anti-forgery-token*}]
[:title "TodoMVC - views.reagent Example"]
(include-css "todos.css" "todosanim.css")
(include-js "cljs/app.js")]
[:body
(anti-forgery-field)
[:div#app [:h1 "This will become todomvc when the ClojureScript is compiled"]]
(javascript-tag "todomvc.client.run();")]))
@ -167,7 +164,7 @@
(def handler
(-> app-routes
(wrap-defaults (assoc-in site-defaults [:security :anti-forgery] (not dev?)))))
(wrap-defaults site-defaults)))