diff --git a/examples/todomvc/README.md b/examples/todomvc/README.md index a170255..39bfed6 100644 --- a/examples/todomvc/README.md +++ b/examples/todomvc/README.md @@ -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). diff --git a/examples/todomvc/project.clj b/examples/todomvc/project.clj index 33666c6..8cd9a60 100644 --- a/examples/todomvc/project.clj +++ b/examples/todomvc/project.clj @@ -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 diff --git a/examples/todomvc/src/todomvc/client.cljs b/examples/todomvc/src/todomvc/client.cljs index c0120bb..128f28b 100644 --- a/examples/todomvc/src/todomvc/client.cljs +++ b/examples/todomvc/src/todomvc/client.cljs @@ -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")))) diff --git a/examples/todomvc/src/todomvc/server.clj b/examples/todomvc/src/todomvc/server.clj index d966cab..1ca5fc0 100644 --- a/examples/todomvc/src/todomvc/server.clj +++ b/examples/todomvc/src/todomvc/server.clj @@ -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)))