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