update example todomvc app
This commit is contained in:
parent
6542fab155
commit
2e429bb5c0
41
examples/todomvc/README.md
Normal file
41
examples/todomvc/README.md
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
# Reagent Data Views 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.
|
||||||
|
|
||||||
|
[1]: http://reagent-project.github.io/
|
||||||
|
|
||||||
|
## Running
|
||||||
|
|
||||||
|
Since Reagent Data Views and the Views library it depends on are all
|
||||||
|
currently in somewhat of an experimental / pre-beta state right now,
|
||||||
|
you will need to first clone the following repositories and manually
|
||||||
|
install the libraries via `lein install`:
|
||||||
|
|
||||||
|
* [views](https://github.com/gered/views)
|
||||||
|
* [views-sql](https://github.com/gered/views-sql)
|
||||||
|
* [reagent-data-views](https://github.com/gered/reagent-data-views)
|
||||||
|
|
||||||
|
As well, you can install [views-honeysql](https://github.com/gered/views-honeysql)
|
||||||
|
if you want to try out using HoneySQL instead of SQL with views. But
|
||||||
|
this example app does not use it so it's not required.
|
||||||
|
|
||||||
|
Once these libraries are install, you can simply build the ClojureScript:
|
||||||
|
|
||||||
|
$ lein cljsbuild once
|
||||||
|
|
||||||
|
And then start up a REPL and run:
|
||||||
|
|
||||||
|
(-main)
|
||||||
|
|
||||||
|
And a new browser window should open to the app.
|
||||||
|
|
||||||
|
Alternatively, to build and run in one go:
|
||||||
|
|
||||||
|
$ lein rundemo
|
||||||
|
|
||||||
|
Open up a second browser and make changes by adding or deleting a Todo,
|
||||||
|
or marking them as completed, etc. and see that the changes are
|
||||||
|
instantly propagated to all clients.
|
|
@ -1,50 +1,56 @@
|
||||||
(defproject todomvc "0.1.0-SNAPSHOT"
|
(defproject todomvc "0.1.0-SNAPSHOT"
|
||||||
:dependencies [[org.clojure/clojure "1.6.0"]
|
:dependencies [[org.clojure/clojure "1.8.0"]
|
||||||
[org.clojure/clojurescript "0.0-2371"]
|
[org.clojure/clojurescript "1.8.51"]
|
||||||
[compojure "1.2.1"]
|
[ring "1.4.0"]
|
||||||
[ring "1.3.1"]
|
[ring/ring-defaults "0.2.0" :exclusions [javax.servlet/servlet-api]]
|
||||||
[ring/ring-defaults "0.1.3" :exclusions [javax.servlet/servlet-api]]
|
[compojure "1.4.0"]
|
||||||
[net.thegeez/clj-browserchannel-jetty-adapter "0.0.6"]
|
[org.immutant/web "2.1.4"]
|
||||||
[clj-browserchannel-messaging "0.0.4"]
|
|
||||||
[clj-pebble "0.2.0"]
|
|
||||||
[cljs-ajax "0.3.3"]
|
|
||||||
[reagent "0.5.0-alpha"]
|
|
||||||
[reagent-data-views "0.1.0-SNAPSHOT"]
|
|
||||||
[views "0.5.0"]
|
|
||||||
[org.clojure/java.jdbc "0.3.5"]
|
|
||||||
[org.postgresql/postgresql "9.2-1003-jdbc4"]
|
|
||||||
[honeysql "0.4.3"]
|
|
||||||
[environ "1.0.0"]]
|
|
||||||
|
|
||||||
:plugins [[lein-cljsbuild "1.0.3"]]
|
[org.clojure/java.jdbc "0.6.1"]
|
||||||
|
[org.postgresql/postgresql "9.4.1208.jre7"]
|
||||||
|
[gered/clj-browserchannel "0.3.1"]
|
||||||
|
[gered/clj-browserchannel-immutant-adapter "0.0.3"]
|
||||||
|
[gered/views "1.5-SNAPSHOT"]
|
||||||
|
[gered/views-sql "0.1.0-SNAPSHOT"]
|
||||||
|
[reagent-data-views "0.2.0-SNAPSHOT"]
|
||||||
|
[reagent-data-views-browserchannel "0.1.0-SNAPSHOT"]
|
||||||
|
|
||||||
:main todomvc.server
|
[clj-pebble "0.2.0"]
|
||||||
|
[reagent "0.6.0-alpha2"]
|
||||||
|
[cljs-ajax "0.5.4"]
|
||||||
|
; only being used to get a <meta> tag value with the CSRF token in it
|
||||||
|
[prismatic/dommy "1.1.0"]
|
||||||
|
|
||||||
:cljsbuild {:builds
|
[environ "1.0.3"]]
|
||||||
{:main
|
|
||||||
{:source-paths ["src"]
|
|
||||||
:compiler
|
|
||||||
{:preamble ["reagent/react.js"]
|
|
||||||
:output-to "resources/public/cljs/client.js"
|
|
||||||
:source-map "resources/public/cljs/client.js.map"
|
|
||||||
:output-dir "resources/public/cljs/client"
|
|
||||||
:optimizations :none
|
|
||||||
:pretty-print true}}}}
|
|
||||||
|
|
||||||
:profiles {:dev {:env {:dev true}}
|
:plugins [[lein-cljsbuild "1.1.3"]
|
||||||
|
[lein-environ "1.0.3"]]
|
||||||
|
|
||||||
:uberjar {:env {:dev false}
|
:main todomvc.server
|
||||||
:hooks [leiningen.cljsbuild]
|
|
||||||
:cljsbuild
|
|
||||||
{:jar true
|
|
||||||
:builds
|
|
||||||
{:main
|
|
||||||
{:compiler
|
|
||||||
^:replace
|
|
||||||
{:output-to "resources/public/cljs/client.js"
|
|
||||||
:preamble ["reagent/react.min.js"]
|
|
||||||
:optimizations :advanced
|
|
||||||
:pretty-print false}}}}}}
|
|
||||||
|
|
||||||
:aliases {"uberjar" ["do" "clean" ["cljsbuild clean"] "uberjar"]
|
:clean-targets ^{:protect false} [:target-path
|
||||||
"cljsdev" ["do" ["cljsbuild" "clean"] ["cljsbuild" "once"] ["cljsbuild" "auto"]]})
|
[:cljsbuild :builds :main :compiler :output-dir]
|
||||||
|
[:cljsbuild :builds :main :compiler :output-to]]
|
||||||
|
:cljsbuild {:builds {:main
|
||||||
|
{:source-paths ["src"]
|
||||||
|
:compiler {:output-to "resources/public/cljs/app.js"
|
||||||
|
:output-dir "resources/public/cljs/target"
|
||||||
|
:source-map true
|
||||||
|
:optimizations :none
|
||||||
|
:pretty-print true}}}}
|
||||||
|
|
||||||
|
:profiles {:dev {:env {:dev "true"}}
|
||||||
|
|
||||||
|
:uberjar {:env {}
|
||||||
|
:aot :all
|
||||||
|
:hooks [leiningen.cljsbuild]
|
||||||
|
:cljsbuild {:jar true
|
||||||
|
:builds {:main
|
||||||
|
{:compiler ^:replace {:output-to "resources/public/cljs/app.js"
|
||||||
|
:optimizations :advanced
|
||||||
|
:pretty-print false}}}}}}
|
||||||
|
|
||||||
|
:aliases {"rundemo" ["do" ["clean"] ["cljsbuild" "once"] ["run"]]
|
||||||
|
"uberjar" ["do" ["clean"] ["uberjar"]]}
|
||||||
|
|
||||||
|
)
|
||||||
|
|
|
@ -5,12 +5,19 @@
|
||||||
<title>todomvc with reagent</title>
|
<title>todomvc with reagent</title>
|
||||||
<link rel="stylesheet" href="todos.css">
|
<link rel="stylesheet" href="todos.css">
|
||||||
<link rel="stylesheet" href="todosanim.css">
|
<link rel="stylesheet" href="todosanim.css">
|
||||||
|
|
||||||
|
<!-- include CSRF token that ring's anti-forgery middleware is expecting.
|
||||||
|
clj-browserchannel's client-side init will pick this meta tag up
|
||||||
|
automatically and include the token in all of browserchannel's
|
||||||
|
requests to the server. -->
|
||||||
|
<meta name="anti-forgery-token" content="{{ csrfToken }}">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>This will become todomvc when the ClojureScript is compiled</h1>
|
<div id="app">
|
||||||
{% if dev %}<script type="text/javascript" src="cljs/client/goog/base.js"></script>{% endif %}
|
<h1>This will become todomvc when the ClojureScript is compiled</h1>
|
||||||
{% if dev %}<script src="http://fb.me/react-0.12.1.js"></script>{% endif %}
|
</div>
|
||||||
<script type="text/javascript" src="cljs/client.js"></script>
|
{% if dev %}<script type="text/javascript" src="cljs/target/goog/base.js"></script>{% endif %}
|
||||||
|
<script type="text/javascript" src="cljs/app.js"></script>
|
||||||
{% if dev %}<script type="text/javascript">goog.require('todomvc.client');</script>{% endif %}
|
{% if dev %}<script type="text/javascript">goog.require('todomvc.client');</script>{% endif %}
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
todomvc.client.run();
|
todomvc.client.run();
|
||||||
|
|
|
@ -1,10 +1,26 @@
|
||||||
(ns todomvc.client
|
(ns todomvc.client
|
||||||
(:require
|
(:require
|
||||||
[reagent.core :as reagent :refer [atom]]
|
[reagent.core :as r]
|
||||||
[clj-browserchannel-messaging.client :as browserchannel]
|
[ajax.core :refer [POST default-interceptors to-interceptor]]
|
||||||
[reagent-data-views.client.core :as rviews]
|
[dommy.core :refer-macros [sel1]]
|
||||||
|
[net.thegeez.browserchannel.client :as browserchannel]
|
||||||
[reagent-data-views.client.component :refer [view-cursor] :refer-macros [defvc]]
|
[reagent-data-views.client.component :refer [view-cursor] :refer-macros [defvc]]
|
||||||
[ajax.core :refer [POST]]))
|
[reagent-data-views.browserchannel.client :as rdv-browserchannel]))
|
||||||
|
|
||||||
|
;; Todo MVC - Reagent Implementation
|
||||||
|
;;
|
||||||
|
;; This is taken from the example code shown on http://reagent-project.github.io/
|
||||||
|
;; It has been modified so that instead of using todo data stored client-side in
|
||||||
|
;; an atom, the data is retrieved from the server.
|
||||||
|
;;
|
||||||
|
;; AJAX requests are used to add/edit/delete the todos. The list is refreshed
|
||||||
|
;; whenever a change is made (by any client currently viewing the app) by a
|
||||||
|
;; view subscription. See the 'todo-app' component near the bottom-middle of this
|
||||||
|
;; file for more details about this.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
;; AJAX operations
|
||||||
|
|
||||||
(defn add-todo [text] (POST "/todos/add" {:format :url :params {:title text}}))
|
(defn add-todo [text] (POST "/todos/add" {:format :url :params {:title text}}))
|
||||||
(defn toggle [id] (POST "/todos/toggle" {:format :url :params {:id id}}))
|
(defn toggle [id] (POST "/todos/toggle" {:format :url :params {:id id}}))
|
||||||
|
@ -14,8 +30,12 @@
|
||||||
(defn complete-all [v] (POST "/todos/mark-all" {:format :url :params {:done? v}}))
|
(defn complete-all [v] (POST "/todos/mark-all" {:format :url :params {:done? v}}))
|
||||||
(defn clear-done [] (POST "/todos/delete-all-done"))
|
(defn clear-done [] (POST "/todos/delete-all-done"))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
;; UI Components
|
||||||
|
|
||||||
(defn todo-input [{:keys [title on-save on-stop]}]
|
(defn todo-input [{:keys [title on-save on-stop]}]
|
||||||
(let [val (atom title)
|
(let [val (r/atom title)
|
||||||
stop #(do (reset! val "")
|
stop #(do (reset! val "")
|
||||||
(if on-stop (on-stop)))
|
(if on-stop (on-stop)))
|
||||||
save #(let [v (-> @val str clojure.string/trim)]
|
save #(let [v (-> @val str clojure.string/trim)]
|
||||||
|
@ -31,7 +51,7 @@
|
||||||
nil)})])))
|
nil)})])))
|
||||||
|
|
||||||
(def todo-edit (with-meta todo-input
|
(def todo-edit (with-meta todo-input
|
||||||
{:component-did-mount #(.focus (reagent/dom-node %))}))
|
{:component-did-mount #(.focus (r/dom-node %))}))
|
||||||
|
|
||||||
(defn todo-stats [{:keys [filt active done]}]
|
(defn todo-stats [{:keys [filt active done]}]
|
||||||
(let [props-for (fn [name]
|
(let [props-for (fn [name]
|
||||||
|
@ -49,7 +69,7 @@
|
||||||
"Clear completed " done])]))
|
"Clear completed " done])]))
|
||||||
|
|
||||||
(defn todo-item []
|
(defn todo-item []
|
||||||
(let [editing (atom false)]
|
(let [editing (r/atom false)]
|
||||||
(fn [{:keys [id done title]}]
|
(fn [{:keys [id done title]}]
|
||||||
[:li {:class (str (if done "completed ")
|
[:li {:class (str (if done "completed ")
|
||||||
(if @editing "editing"))}
|
(if @editing "editing"))}
|
||||||
|
@ -63,23 +83,48 @@
|
||||||
:on-save #(save id %)
|
:on-save #(save id %)
|
||||||
:on-stop #(reset! editing false)}])])))
|
:on-stop #(reset! editing false)}])])))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
;; Main TODO app component
|
||||||
|
;;
|
||||||
|
;; Note that this component is defined using 'defvc' instead of 'defn'. This is a
|
||||||
|
;; macro provided by reagent-data-views which is required to be used by any Reagent
|
||||||
|
;; component that will directly subscribe/unsubscribe to views. It handles all the
|
||||||
|
;; housekeeping operations that working with views on the client entails.
|
||||||
|
;;
|
||||||
|
;; The call to 'view-cursor' is where the rest of the magic happens. This function
|
||||||
|
;; will:
|
||||||
|
;;
|
||||||
|
;; - Send a subscription request to the server for the specified view and parameters
|
||||||
|
;; if a subscription for the view (and the exact provided parameters) does not
|
||||||
|
;; already exist.
|
||||||
|
;; - Returns the most recent data for this view in a Reagent cursor. When the data
|
||||||
|
;; is changed and the server sends a view refresh, components dereferencing this
|
||||||
|
;; cursor will be rerendered, just like any other Reagent atom/cursor.
|
||||||
|
;; - If the values of the (optional) parameters passed to view-cursor change, a
|
||||||
|
;; view resubscription (with the new parameters) will be triggered automatically
|
||||||
|
;; and the server will send us new view data.
|
||||||
|
;;
|
||||||
|
;; NOTE:
|
||||||
|
;; view-cursor cannot be used in a Reagent component that was created using defn.
|
||||||
|
|
||||||
(defvc todo-app [props]
|
(defvc todo-app [props]
|
||||||
(let [filt (atom :all)]
|
(let [filt (r/atom :all)]
|
||||||
(fn []
|
(fn []
|
||||||
(let [items (view-cursor [:todos])
|
(let [items (view-cursor :todos)
|
||||||
done (->> @items (filter :done) count)
|
done (->> @items (filter :done) count)
|
||||||
active (- (count @items) done)]
|
active (- (count @items) done)]
|
||||||
[:div
|
[:div
|
||||||
[:section#todoapp
|
[:section#todoapp
|
||||||
[:header#header
|
[:header#header
|
||||||
[:h1 "todos"]
|
[:h1 "todos"]
|
||||||
[todo-input {:id "new-todo"
|
[todo-input {:id "new-todo"
|
||||||
:placeholder "What needs to be done?"
|
:placeholder "What needs to be done?"
|
||||||
:on-save add-todo}]]
|
:on-save add-todo}]]
|
||||||
(when (-> @items count pos?)
|
(when (-> @items count pos?)
|
||||||
[:div
|
[:div
|
||||||
[:section#main
|
[:section#main
|
||||||
[:input#toggle-all {:type "checkbox" :checked (zero? active)
|
[:input#toggle-all {:type "checkbox" :checked (zero? active)
|
||||||
:on-change #(complete-all (pos? active))}]
|
:on-change #(complete-all (pos? active))}]
|
||||||
[:label {:for "toggle-all"} "Mark all as complete"]
|
[:label {:for "toggle-all"} "Mark all as complete"]
|
||||||
[:ul#todo-list
|
[:ul#todo-list
|
||||||
|
@ -96,9 +141,36 @@
|
||||||
[:footer#info
|
[:footer#info
|
||||||
[:p "Double-click to edit a todo"]]]))))
|
[:p "Double-click to edit a todo"]]]))))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
;; Some unfortunately necessary set up to ensure we send the CSRF token back with
|
||||||
|
;; AJAX requests (clj-browserchannel handles this automatically for it's own HTTP
|
||||||
|
;; requests, so the set up we do is only for our own application code).
|
||||||
|
|
||||||
|
(defn get-anti-forgery-token []
|
||||||
|
(if-let [tag (sel1 "meta[name='anti-forgery-token']")]
|
||||||
|
(.-content tag)))
|
||||||
|
|
||||||
|
(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))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
;; Page load
|
||||||
|
|
||||||
(defn ^:export run []
|
(defn ^:export run []
|
||||||
(browserchannel/init!
|
; Configure reagent-data-views and then BrowserChannel.
|
||||||
:on-connect
|
(rdv-browserchannel/configure!)
|
||||||
(fn []
|
|
||||||
(rviews/init!)
|
; NOTE: We are passing in an empty map for the BrowserChannel event handlers only
|
||||||
(reagent/render-component [todo-app] (.-body js/document)))))
|
; because this todo app is not using BrowserChannel for any purpose other
|
||||||
|
; then to provide client/server messaging for reagent-data-views. If we
|
||||||
|
; wanted to use it for client/server messaging in our application as well,
|
||||||
|
; we could pass in any event handlers we want here and it would not intefere
|
||||||
|
; with reagent-data-views.
|
||||||
|
(browserchannel/connect! {} {:middleware [rdv-browserchannel/middleware]})
|
||||||
|
|
||||||
|
(r/render-component [todo-app] (.getElementById js/document "app")))
|
||||||
|
|
|
@ -3,16 +3,19 @@
|
||||||
(:require
|
(:require
|
||||||
[compojure.core :refer [routes GET POST]]
|
[compojure.core :refer [routes GET POST]]
|
||||||
[compojure.route :as route]
|
[compojure.route :as route]
|
||||||
[net.thegeez.jetty-async-adapter :refer [run-jetty-async]]
|
|
||||||
[ring.middleware.defaults :refer [wrap-defaults site-defaults]]
|
[ring.middleware.defaults :refer [wrap-defaults site-defaults]]
|
||||||
|
[ring.middleware.anti-forgery :refer [*anti-forgery-token*]]
|
||||||
[ring.util.response :refer [response]]
|
[ring.util.response :refer [response]]
|
||||||
|
[net.thegeez.browserchannel.server :refer [wrap-browserchannel]]
|
||||||
|
[net.thegeez.browserchannel.immutant-async-adapter :refer [wrap-immutant-async-adapter]]
|
||||||
|
[immutant.web :as immutant]
|
||||||
[clj-pebble.core :as pebble]
|
[clj-pebble.core :as pebble]
|
||||||
[clj-browserchannel-messaging.server :as browserchannel]
|
|
||||||
[environ.core :refer [env]]
|
[environ.core :refer [env]]
|
||||||
[honeysql.helpers :refer :all]
|
[clojure.java.jdbc :as jdbc]
|
||||||
[clojure.java.jdbc :as sql]
|
[views.sql.core :refer [vexec! with-view-transaction]]
|
||||||
[reagent-data-views.server.core :as rviews :refer [views-config]]
|
[views.sql.view :refer [view]]
|
||||||
[views.db.core :refer [vexec! with-view-transaction]]))
|
[reagent-data-views.browserchannel.server :as rdv-browserchannel]))
|
||||||
|
|
||||||
|
|
||||||
(def db {:classname "org.postgresql.Driver"
|
(def db {:classname "org.postgresql.Driver"
|
||||||
:subprotocol "postgresql"
|
:subprotocol "postgresql"
|
||||||
|
@ -21,74 +24,82 @@
|
||||||
:password "s3cr3t"})
|
:password "s3cr3t"})
|
||||||
|
|
||||||
|
|
||||||
;; View templates (functions which return HoneySQL SELECT query maps)
|
|
||||||
|
|
||||||
(defn ^:refresh-only todos-view []
|
;; View functions.
|
||||||
(-> (select :id :title :done)
|
;;
|
||||||
(from :todos)))
|
;; These are functions which accept any number of parameters provided when the view
|
||||||
|
;; is subscribed to and run whenever a subscriber needs refreshed data for it.
|
||||||
|
;;
|
||||||
|
;; A view function's return value requirement depends on what views IView
|
||||||
|
;; implementation is being used.
|
||||||
|
;;
|
||||||
|
;; This example app is using views-sql, so view templates should return a SQL SELECT
|
||||||
|
;; query in a clojure.java.jdbc "sqlvec" which is a vector where the first string is
|
||||||
|
;; the actual SQL query and is followed by any number of parameters to be used in
|
||||||
|
;; the query.
|
||||||
|
|
||||||
|
(defn get-todos []
|
||||||
|
["SELECT id, title, done FROM todos ORDER BY title"])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
;; Views list.
|
||||||
|
;;
|
||||||
|
;; A definition/declaration of the views in the system. Each view is given an id and
|
||||||
|
;; points to a function that returns the query that will be used to retrieve the view's
|
||||||
|
;; data. Also other properties can be provided to each view, such as the database connection.
|
||||||
|
;;
|
||||||
|
;; The view id and parameters to the view function get used later on to form a
|
||||||
|
;; "view signature" or "view-sig" when the client subscribes to a view.
|
||||||
|
|
||||||
; the keys in this map are the names of the views, which we can refer to in our cljs code
|
|
||||||
; (the key + args to the view fn combined make up a "view signature" or "view-sig"
|
|
||||||
; e.g. [:items] in this case as it has zero arguments.
|
|
||||||
(def views
|
(def views
|
||||||
{:todos {:fn #'todos-view}})
|
[(view :todos db #'get-todos)])
|
||||||
|
|
||||||
|
|
||||||
;; SQL operations (affecting the views defined above, so we use vexec! instead of jdbc calls)
|
|
||||||
|
;; SQL operations triggered by AJAX requests.
|
||||||
|
;;
|
||||||
|
;; These functions are just your ordinary AJAX request handlers that do the various
|
||||||
|
;; CRUD operations on the example app's data. The only difference is that instead
|
||||||
|
;; of using clojure.java.jdbc/execute!, we instead use vexec!.
|
||||||
|
;;
|
||||||
|
;; vexec! performs the exact same operation as execute!, except that it also
|
||||||
|
;; analyzes the SQL query being run and dispatches "hints" to the view system which
|
||||||
|
;; trigger view refrehses for all subscribers of the views that the hints match.
|
||||||
|
|
||||||
(defn add-todo! [title]
|
(defn add-todo! [title]
|
||||||
(vexec!
|
(vexec! db ["INSERT INTO todos (title) VALUES (?)" title])
|
||||||
@views-config
|
(response "ok"))
|
||||||
(-> (insert-into :todos)
|
|
||||||
(values [{:title title}])))
|
|
||||||
(response "added todo"))
|
|
||||||
|
|
||||||
(defn delete-todo! [id]
|
(defn delete-todo! [id]
|
||||||
(vexec!
|
(vexec! db ["DELETE FROM todos WHERE id = ?" id])
|
||||||
@views-config
|
(response "ok"))
|
||||||
(-> (delete-from :todos)
|
|
||||||
(where [:= :id id])))
|
|
||||||
(response "deleted todo"))
|
|
||||||
|
|
||||||
(defn update-todo! [id title]
|
(defn update-todo! [id title]
|
||||||
(vexec!
|
(vexec! db ["UPDATE todos SET title = ? WHERE id = ?" title id])
|
||||||
@views-config
|
(response "ok"))
|
||||||
(-> (update :todos)
|
|
||||||
(sset {:title title})
|
|
||||||
(where [:= :id id])))
|
|
||||||
(response "updated todo"))
|
|
||||||
|
|
||||||
(defn toggle-todo! [id]
|
(defn toggle-todo! [id]
|
||||||
; note that we could have written this operation using a single UPDATE query,
|
; note that a transaction is obviously not necessary here as we could have used
|
||||||
; but writing it this way also serves to demonstrate:
|
; just a single UPDATE query. however, it is being done this way to demonstrate
|
||||||
; - using transactions with vexec!
|
; using transactions with vexec!.
|
||||||
; - that the db connection is available under :db in the views config map and can
|
(with-view-transaction
|
||||||
; be used to run any ordinary query directly with jdbc (of course)
|
[dt db]
|
||||||
(with-view-transaction [vt @views-config]
|
(let [done? (:done (first (jdbc/query dt ["SELECT done FROM todos WHERE id = ?" id])))]
|
||||||
(let [done? (:done (first (sql/query (:db vt) ["SELECT done FROM todos WHERE id = ?" id])))]
|
(vexec! dt ["UPDATE todos SET done = ? WHERE id = ?" (not done?) id]))
|
||||||
(vexec!
|
(response "ok")))
|
||||||
vt
|
|
||||||
(-> (update :todos)
|
|
||||||
(sset {:done (not done?)})
|
|
||||||
(where [:= :id id]))))
|
|
||||||
(response "toggled todo")))
|
|
||||||
|
|
||||||
(defn mark-all! [done?]
|
(defn mark-all! [done?]
|
||||||
(vexec!
|
(vexec! db ["UPDATE todos SET done = ?" done?])
|
||||||
@views-config
|
(response "ok"))
|
||||||
(-> (update :todos)
|
|
||||||
(sset {:done done?})))
|
|
||||||
(response "completed all todos"))
|
|
||||||
|
|
||||||
(defn delete-all-done! []
|
(defn delete-all-done! []
|
||||||
(vexec!
|
(vexec! db ["DELETE FROM todos WHERE done = true"])
|
||||||
@views-config
|
(response "ok"))
|
||||||
(-> (delete-from :todos)
|
|
||||||
(where [:= :done true])))
|
|
||||||
(response "deleted all done todos"))
|
|
||||||
|
|
||||||
|
|
||||||
;; Compojure routes / Jetty server & main
|
|
||||||
|
;; Compojure routes and Ring handler
|
||||||
|
|
||||||
(def app-routes
|
(def app-routes
|
||||||
(routes
|
(routes
|
||||||
|
@ -100,23 +111,46 @@
|
||||||
(POST "/todos/mark-all" [done?] (mark-all! (Boolean/parseBoolean done?)))
|
(POST "/todos/mark-all" [done?] (mark-all! (Boolean/parseBoolean done?)))
|
||||||
(POST "/todos/delete-all-done" [] (delete-all-done!))
|
(POST "/todos/delete-all-done" [] (delete-all-done!))
|
||||||
|
|
||||||
(GET "/" [] (pebble/render-resource "html/app.html" {:dev (env :dev)}))
|
; main page
|
||||||
|
(GET "/" [] (pebble/render-resource
|
||||||
|
"html/app.html"
|
||||||
|
{:dev (boolean (env :dev))
|
||||||
|
:csrfToken *anti-forgery-token*}))
|
||||||
|
|
||||||
(route/resources "/")
|
(route/resources "/")
|
||||||
(route/not-found "not found")))
|
(route/not-found "not found")))
|
||||||
|
|
||||||
(defn run-server []
|
(def handler
|
||||||
(browserchannel/init!
|
|
||||||
:middleware [rviews/views-middleware])
|
|
||||||
(rviews/init! db views)
|
|
||||||
|
|
||||||
(-> app-routes
|
(-> app-routes
|
||||||
(browserchannel/wrap-browserchannel)
|
(wrap-defaults site-defaults)
|
||||||
(wrap-defaults (assoc-in site-defaults [:security :anti-forgery] false))
|
; NOTE: We are passing in an empty map for the BrowserChannel event handlers only
|
||||||
(run-jetty-async
|
; because this todo app is not using BrowserChannel for any purpose other
|
||||||
{:port 8080
|
; then to provide client/server messaging for reagent-data-views. If we
|
||||||
:auto-reload? true
|
; wanted to use it for client/server messaging in our application as well,
|
||||||
:join? false}))
|
; we could pass in any event handlers we want here and it would not intefere
|
||||||
(println "Web app is running at http://localhost:8080/"))
|
; with reagent-data-views.
|
||||||
|
(wrap-browserchannel {} {:middleware [rdv-browserchannel/middleware]})
|
||||||
|
(wrap-immutant-async-adapter)))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
;; Web server startup & main
|
||||||
|
|
||||||
|
(defn run-server []
|
||||||
|
(pebble/set-options! :cache (env :dev))
|
||||||
|
|
||||||
|
; init-views takes care of initialization views and reagent-data-views at the same
|
||||||
|
; time. As a result, we do not need to also call views.core/init! anywhere. The
|
||||||
|
; same options you are able to pass to views.core/init! can also be passed in here
|
||||||
|
; and they will be forwarded along.
|
||||||
|
;
|
||||||
|
; if you need to shutdown the views system (e.g. if you're using something like
|
||||||
|
; Component or Mount), you can just call views.core/shutdown!.
|
||||||
|
(rdv-browserchannel/init-views! views)
|
||||||
|
|
||||||
|
(if (env :dev)
|
||||||
|
(immutant/run-dmc #'handler {:port 8080})
|
||||||
|
(immutant/run #'handler {:port 8080})))
|
||||||
|
|
||||||
(defn -main [& args]
|
(defn -main [& args]
|
||||||
(run-server))
|
(run-server))
|
||||||
|
|
Loading…
Reference in a new issue