update/fix todomvc example
This commit is contained in:
parent
17667c274a
commit
414c491483
|
@ -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).
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"))))
|
||||||
|
|
|
@ -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)))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue