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

View file

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

View file

@ -1,7 +1,8 @@
(ns todomvc.client (ns todomvc.client
(:require (:require
[reagent.core :as r] [reagent.core :as r]
[ajax.core :refer [POST default-interceptors to-interceptor]] [reagent.dom :as rdom]
[ajax.core :as ajax]
[taoensso.sente :as sente] [taoensso.sente :as sente]
[views.reagent.client.component :refer [view-cursor] :refer-macros [defvc]] [views.reagent.client.component :refer [view-cursor] :refer-macros [defvc]]
[views.reagent.sente.client :as vr])) [views.reagent.sente.client :as vr]))
@ -30,13 +31,13 @@
;; AJAX operations ;; AJAX operations
(defn add-todo [text] (POST "/todos/add" {:format :url :params {:title text}})) (defn add-todo [text] (ajax/POST "/todos/add" {:format :url :params {:title text}}))
(defn toggle [id] (POST "/todos/toggle" {:format :url :params {:id id}})) (defn toggle [id] (ajax/POST "/todos/toggle" {:format :url :params {:id id}}))
(defn save [id title] (POST "/todos/update" {:format :url :params {:id id :title title}})) (defn save [id title] (ajax/POST "/todos/update" {:format :url :params {:id id :title title}}))
(defn delete [id] (POST "/todos/delete" {:format :url :params {:id id}})) (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 complete-all [v] (ajax/POST "/todos/mark-all" {:format :url :params {:done? v}}))
(defn clear-done [] (POST "/todos/delete-all-done")) (defn clear-done [] (ajax/POST "/todos/delete-all-done"))
@ -51,7 +52,7 @@
(if-not (empty? v) (on-save v)) (if-not (empty? v) (on-save v))
(stop))] (stop))]
(fn [props] (fn [props]
[:input (merge props [:input (merge (dissoc props :on-save)
{:type "text" :value @val :on-blur save {:type "text" :value @val :on-blur save
:on-change #(reset! val (-> % .-target .-value)) :on-change #(reset! val (-> % .-target .-value))
:on-key-down #(case (.-which %) :on-key-down #(case (.-which %)
@ -60,7 +61,7 @@
nil)})]))) nil)})])))
(def todo-edit (with-meta todo-input (def todo-edit (with-meta todo-input
{:component-did-mount #(.focus (r/dom-node %))})) {:component-did-mount #(.focus (rdom/dom-node %))}))
(defn todo-stats (defn todo-stats
[{:keys [filt active done]}] [{:keys [filt active done]}]
@ -95,6 +96,24 @@
:on-stop #(reset! editing false)}])]))) :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 ;; 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 ;; Sente event/message handler
;; ;;
;; Note that if you're only using Sente to make use of views.reagent in your app ;; Note that if you're only using Sente to make use of views.reagent in your app
@ -182,10 +185,8 @@
(defn sente-event-msg-handler (defn sente-event-msg-handler
[{:keys [event id client-id] :as ev}] [{:keys [event id client-id] :as ev}]
(let [[ev-id ev-data] event]
(cond (cond
(and (= :chsk/state ev-id) (vr/chsk-open-event? ev)
(:open? ev-data))
(vr/on-open! @sente-socket ev) (vr/on-open! @sente-socket ev)
(= :chsk/recv id) (= :chsk/recv id)
@ -194,7 +195,23 @@
; handled it. ; handled it.
; ;
; you could put your code to handle your app's own events here ; 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,9 +219,14 @@
(defn ^:export run (defn ^:export run
[] []
(enable-console-print!)
(let [csrf-token (get-csrf-token)]
(if csrf-token (add-csrf-token-ajax-interceptor! csrf-token))
; Sente setup. create the socket, storing it in an atom and set up a event ; Sente setup. create the socket, storing it in an atom and set up a event
; handler using sente's own message router functionality. ; handler using sente's own message router functionality.
(reset! sente-socket (sente/make-channel-socket! "/chsk" {})) (reset! sente-socket (sente/make-channel-socket! "/chsk" csrf-token {}))
; set up a handler for sente events ; set up a handler for sente events
(sente/start-chsk-router! (:ch-recv @sente-socket) sente-event-msg-handler) (sente/start-chsk-router! (:ch-recv @sente-socket) sente-event-msg-handler)
@ -212,4 +234,4 @@
; Configure views.reagent for use with Sente. ; Configure views.reagent for use with Sente.
(vr/init! @sente-socket {}) (vr/init! @sente-socket {})
(r/render-component [todo-app] (.getElementById js/document "app"))) (rdom/render [todo-app] (.getElementById js/document "app"))))

View file

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