update examples (switched to sente, copy of todomvc w/ browserchannel)

This commit is contained in:
Gered 2016-05-31 14:43:49 -04:00
parent 568ab6c889
commit 282debca28
16 changed files with 1298 additions and 54 deletions

View file

@ -27,10 +27,10 @@ you will need to first clone the following repositories and manually
install the libraries via `lein install`: install the libraries via `lein install`:
* [views](https://github.com/gered/views) * [views](https://github.com/gered/views)
* [views-sql](https://github.com/gered/views-sql) * [views.sql](https://github.com/gered/views.sql)
* [views.reagent](https://github.com/gered/views.reagent) * [views.reagent](https://github.com/gered/views.reagent)
As well, you can install [views-honeysql](https://github.com/gered/views-honeysql) 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 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. this example app does not use it so it's not required.

View file

@ -9,12 +9,11 @@
[org.clojure/java.jdbc "0.6.1"] [org.clojure/java.jdbc "0.6.1"]
[org.postgresql/postgresql "9.4.1208.jre7"] [org.postgresql/postgresql "9.4.1208.jre7"]
[gered/clj-browserchannel "0.3.2"] [com.taoensso/sente "1.8.1"]
[gered/clj-browserchannel-immutant-adapter "0.0.3"]
[gered/views "1.5-SNAPSHOT"] [gered/views "1.5-SNAPSHOT"]
[gered/views.sql "0.1.0-SNAPSHOT"] [gered/views.sql "0.1.0-SNAPSHOT"]
[gered/views.reagent "0.2.0-SNAPSHOT"] [gered/views.reagent "0.2.0-SNAPSHOT"]
[gered/views.reagent.browserchannel "0.1.0-SNAPSHOT"] [gered/views.reagent.sente "0.1.0-SNAPSHOT"]
[hiccup "1.0.5"] [hiccup "1.0.5"]
[reagent "0.6.0-alpha2"] [reagent "0.6.0-alpha2"]

View file

@ -3,9 +3,9 @@
[clojure.string :as string] [clojure.string :as string]
[reagent.core :as r] [reagent.core :as r]
[ajax.core :refer [POST default-interceptors to-interceptor]] [ajax.core :refer [POST default-interceptors to-interceptor]]
[net.thegeez.browserchannel.client :as browserchannel] [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.browserchannel.client :as vr])) [views.reagent.sente.client :as vr]))
;; Class Registry - views.reagent example app ;; Class Registry - views.reagent example app
;; ;;
@ -23,6 +23,12 @@
;; Sente socket
(defonce sente-socket (atom {}))
;; AJAX actions ;; AJAX actions
(defn add-person! [person] (POST "/people/add" {:params {:person person}})) (defn add-person! [person] (POST "/people/add" {:params {:person person}}))
@ -372,11 +378,30 @@
;; Sente event/message handler
(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)
(= :chsk/recv id)
(when-not (vr/on-receive! @sente-socket ev)
; TODO: any code here needed to handle app-specific receive events
))))
;; Page load ;; Page load
(defn ^:export run (defn ^:export run
[] []
(vr/init!) (reset! sente-socket (sente/make-channel-socket! "/chsk" {}))
(browserchannel/connect! {} {:middleware [vr/middleware]}) (sente/start-chsk-router! (:ch-recv @sente-socket) sente-event-msg-handler)
(vr/init! @sente-socket {})
(r/render-component [class-registry-app] (.getElementById js/document "app"))) (r/render-component [class-registry-app] (.getElementById js/document "app")))

View file

@ -7,8 +7,8 @@
[ring.middleware.format :refer [wrap-restful-format]] [ring.middleware.format :refer [wrap-restful-format]]
[ring.util.anti-forgery :refer [anti-forgery-field]] [ring.util.anti-forgery :refer [anti-forgery-field]]
[ring.util.response :refer [response]] [ring.util.response :refer [response]]
[net.thegeez.browserchannel.server :refer [wrap-browserchannel]] [taoensso.sente :as sente]
[net.thegeez.browserchannel.immutant-async-adapter :refer [wrap-immutant-async-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]]
@ -16,7 +16,7 @@
[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.browserchannel.server :as vr])) [views.reagent.sente.server :as vr]))
(def dev? (boolean (env :dev))) (def dev? (boolean (env :dev)))
@ -28,6 +28,10 @@
;; Sente socket
(defonce sente-socket (atom {}))
;; View system atom ;; View system atom
(defonce view-system (atom {})) (defonce view-system (atom {}))
@ -176,12 +180,42 @@
(route/resources "/") (route/resources "/")
(route/not-found "not found"))) (route/not-found "not found")))
;; Ring middleware to intercept requests to Sente's channel socket routes
;;
;; Because our ring handler below is also using wrap-restful-format, we need to make
;; sure we catch requests intended to Sente before wrap-restful-format has a chance to
;; modify any of the request params our we'll get errors.
;; This is obviously not the only approach to handling this problem, but it is one that
;; I personally find nice, easy and flexible.
(defn wrap-sente
[handler uri]
(fn [request]
(let [uri-match? (.startsWith (str (:uri request)) uri)
method (:request-method request)]
(cond
(and uri-match? (= :get method)) ((:ajax-get-or-ws-handshake-fn @sente-socket) request)
(and uri-match? (= :post method)) ((:ajax-post-fn @sente-socket) request)
:else (handler request)))))
(def handler (def handler
(-> app-routes (-> app-routes
(wrap-restful-format :formats [:transit-json]) (wrap-restful-format :formats [:transit-json])
(wrap-defaults (assoc-in site-defaults [:security :anti-forgery] (not dev?))) (wrap-sente "/chsk")
(wrap-browserchannel {} {:middleware [(vr/->middleware view-system)]}) (wrap-defaults (assoc-in site-defaults [:security :anti-forgery] (not dev?)))))
(wrap-immutant-async-adapter)))
;; Sente event/message handler
(defn sente-event-msg-handler
[{:keys [event id uid client-id] :as ev}]
(if (= id :chsk/uidport-close)
(vr/on-close! view-system ev)
(when-not (vr/on-receive! view-system ev)
; TODO: any code here needed to handle app-specific receive events
)))
@ -189,7 +223,14 @@
(defn run-server (defn run-server
[] []
(vr/init! view-system {:views views}) (reset! sente-socket
(sente/make-channel-socket!
sente-web-server-adapter
{:user-id-fn (fn [request] (get-in request [:params :client-id]))}))
(sente/start-chsk-router! (:ch-recv @sente-socket) sente-event-msg-handler)
(vr/init! view-system @sente-socket {:views views})
(immutant/run handler {:port 8080})) (immutant/run handler {:port 8080}))
(defn -main (defn -main

View file

@ -0,0 +1,18 @@
.DS_Store
/target
/classes
/checkouts
pom.xml
pom.xml.asc
*.jar
*.class
/.lein-*
/.nrepl-port
.settings/
.project
.classpath
.idea/
*.iml
*.ipr
*.iws
/resources/public/cljs

View file

@ -0,0 +1,63 @@
# views.reagent Example - Todo MVC (BrowserChannel)
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/
> **NOTE:** This is a copy of the [other Todo MVC example][2] and is the same
> in every respect, except that this one is using [BrowserChannel][3] instead of
> Sente as the underlying client/server messaging implementation.
[2]: https://github.com/gered/views.reagent/tree/master/examples/todomvc
[3]: https://github.com/gered/views.reagent/tree/master/views.reagent.browserchannel
## Running
### A quick note on the dependencies used
Since views.reagent 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)
* [views.reagent](https://github.com/gered/views.reagent)
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.
### 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:
$ psql < create_db.sql
(Of course, add any username/host parameters you might need)
### Starting It Up
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.
If you want to run this application in a REPL, just be sure to build
the ClojureScript:
$ lein cljsbuild once
And then in the REPL you can just run:
(-main)
to start the web app (you should be put in the correct namespace
immediately).

View file

@ -0,0 +1,21 @@
-- For PostgreSQL
-- run with psql. e.g. 'psql < create_db.sql'
CREATE ROLE todomvc LOGIN PASSWORD 's3cr3t';
CREATE DATABASE todomvc OWNER todomvc;
-- assumes you're piping this script into psql ...
\c todomvc;
CREATE TABLE todos
(
id SERIAL PRIMARY KEY NOT NULL,
title TEXT NOT NULL,
done BOOLEAN DEFAULT FALSE NOT NULL
);
ALTER TABLE todos OWNER TO todomvc;
INSERT INTO todos (title, done) VALUES ('Rename Cloact to Reagent', TRUE);
INSERT INTO todos (title, done) VALUES ('Add undo demo', TRUE);
INSERT INTO todos (title, done) VALUES ('Make all rendering async', TRUE);
INSERT INTO todos (title, done) VALUES ('Allow any arguments to component functions', TRUE);

View file

@ -0,0 +1,56 @@
(defproject todomvc-browserchannel "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"]
[org.clojure/java.jdbc "0.6.1"]
[org.postgresql/postgresql "9.4.1208.jre7"]
[gered/clj-browserchannel "0.3.2"]
[gered/clj-browserchannel-immutant-adapter "0.0.3"]
[gered/views "1.5-SNAPSHOT"]
[gered/views.sql "0.1.0-SNAPSHOT"]
[gered/views.reagent "0.2.0-SNAPSHOT"]
[gered/views.reagent.browserchannel "0.1.0-SNAPSHOT"]
[hiccup "1.0.5"]
[reagent "0.6.0-alpha2"]
[cljs-ajax "0.5.4"]
[environ "1.0.3"]]
:plugins [[lein-cljsbuild "1.1.3"]
[lein-environ "1.0.3"]]
:main todomvc.server
:clean-targets ^{:protect false} [:target-path
[:cljsbuild :builds :main :compiler :output-dir]
[:cljsbuild :builds :main :compiler :output-to]]
:cljsbuild {:builds {:main
{:source-paths ["src"]
:compiler {:main todomvc.client
:output-to "resources/public/cljs/app.js"
:output-dir "resources/public/cljs/target"
:asset-path "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"]]}
)

View file

@ -0,0 +1,558 @@
@charset "utf-8";
html,
body {
margin: 0;
padding: 0;
}
button {
margin: 0;
padding: 0;
border: 0;
background: none;
font-size: 100%;
vertical-align: baseline;
font-family: inherit;
color: inherit;
-webkit-appearance: none;
-ms-appearance: none;
-o-appearance: none;
appearance: none;
}
body {
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
line-height: 1.4em;
background: #eaeaea;
/* background: #eaeaea url('bg.png'); */
color: #4d4d4d;
width: 550px;
margin: 0 auto;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
-ms-font-smoothing: antialiased;
-o-font-smoothing: antialiased;
font-smoothing: antialiased;
}
button,
input[type="checkbox"] {
outline: none;
}
#todoapp {
background: #fff;
background: rgba(255, 255, 255, 0.9);
margin: 130px 0 40px 0;
border: 1px solid #ccc;
position: relative;
border-top-left-radius: 2px;
border-top-right-radius: 2px;
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.2),
0 25px 50px 0 rgba(0, 0, 0, 0.15);
}
#todoapp:before {
content: '';
border-left: 1px solid #f5d6d6;
border-right: 1px solid #f5d6d6;
width: 2px;
position: absolute;
top: 0;
left: 40px;
height: 100%;
}
#todoapp input::-webkit-input-placeholder {
font-style: italic;
}
#todoapp input::-moz-placeholder {
font-style: italic;
color: #a9a9a9;
}
#todoapp h1 {
position: absolute;
top: -120px;
width: 100%;
font-size: 70px;
font-weight: bold;
text-align: center;
color: #b3b3b3;
color: rgba(255, 255, 255, 0.3);
text-shadow: -1px -1px rgba(0, 0, 0, 0.2);
-webkit-text-rendering: optimizeLegibility;
-moz-text-rendering: optimizeLegibility;
-ms-text-rendering: optimizeLegibility;
-o-text-rendering: optimizeLegibility;
text-rendering: optimizeLegibility;
}
#header {
padding-top: 15px;
border-radius: inherit;
}
#header:before {
content: '';
position: absolute;
top: 0;
right: 0;
left: 0;
height: 15px;
z-index: 2;
border-bottom: 1px solid #6c615c;
background: #8d7d77;
background: -webkit-gradient(linear, left top, left bottom, from(rgba(132, 110, 100, 0.8)),to(rgba(101, 84, 76, 0.8)));
background: -webkit-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
background: linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#9d8b83', EndColorStr='#847670');
border-top-left-radius: 1px;
border-top-right-radius: 1px;
}
#new-todo,
.edit {
position: relative;
margin: 0;
width: 100%;
font-size: 24px;
font-family: inherit;
line-height: 1.4em;
border: 0;
outline: none;
color: inherit;
padding: 6px;
border: 1px solid #999;
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
-moz-box-sizing: border-box;
-ms-box-sizing: border-box;
-o-box-sizing: border-box;
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
-ms-font-smoothing: antialiased;
-o-font-smoothing: antialiased;
font-smoothing: antialiased;
}
#new-todo {
padding: 16px 16px 16px 60px;
border: none;
background: rgba(0, 0, 0, 0.02);
z-index: 2;
box-shadow: none;
}
#main {
position: relative;
z-index: 2;
border-top: 1px dotted #adadad;
}
label[for='toggle-all'] {
display: none;
}
#toggle-all {
position: absolute;
top: -42px;
left: -4px;
width: 40px;
text-align: center;
/* Mobile Safari */
border: none;
}
#toggle-all:before {
content: '»';
font-size: 28px;
color: #d9d9d9;
padding: 0 25px 7px;
}
#toggle-all:checked:before {
color: #737373;
}
#todo-list {
margin: 0;
padding: 0;
list-style: none;
}
#todo-list li {
position: relative;
font-size: 24px;
border-bottom: 1px dotted #ccc;
}
#todo-list li:last-child {
border-bottom: none;
}
#todo-list li.editing {
border-bottom: none;
padding: 0;
}
#todo-list li.editing .edit {
display: block;
width: 506px;
padding: 13px 17px 12px 17px;
margin: 0 0 0 43px;
}
#todo-list li.editing .view {
display: none;
}
#todo-list li .toggle {
text-align: center;
width: 40px;
/* auto, since non-WebKit browsers doesn't support input styling */
height: auto;
position: absolute;
top: 0;
bottom: 0;
margin: auto 0;
/* Mobile Safari */
border: none;
-webkit-appearance: none;
-ms-appearance: none;
-o-appearance: none;
appearance: none;
}
#todo-list li .toggle:after {
content: '✔';
/* 40 + a couple of pixels visual adjustment */
line-height: 43px;
font-size: 20px;
color: #d9d9d9;
text-shadow: 0 -1px 0 #bfbfbf;
}
#todo-list li .toggle:checked:after {
color: #85ada7;
text-shadow: 0 1px 0 #669991;
bottom: 1px;
position: relative;
}
#todo-list li label {
white-space: pre;
word-break: break-word;
padding: 15px 60px 15px 15px;
margin-left: 45px;
display: block;
line-height: 1.2;
-webkit-transition: color 0.4s;
transition: color 0.4s;
}
#todo-list li.completed label {
color: #a9a9a9;
text-decoration: line-through;
}
#todo-list li .destroy {
display: none;
position: absolute;
top: 0;
right: 10px;
bottom: 0;
width: 40px;
height: 40px;
margin: auto 0;
font-size: 22px;
color: #a88a8a;
-webkit-transition: all 0.2s;
transition: all 0.2s;
}
#todo-list li .destroy:hover {
text-shadow: 0 0 1px #000,
0 0 10px rgba(199, 107, 107, 0.8);
-webkit-transform: scale(1.3);
-ms-transform: scale(1.3);
transform: scale(1.3);
}
#todo-list li .destroy:after {
content: '✖';
}
#todo-list li:hover .destroy {
display: block;
}
#todo-list li .edit {
display: none;
}
#todo-list li.editing:last-child {
margin-bottom: -1px;
}
#footer {
color: #777;
padding: 0 15px;
position: absolute;
right: 0;
bottom: -31px;
left: 0;
height: 20px;
z-index: 1;
text-align: center;
}
#footer:before {
content: '';
position: absolute;
right: 0;
bottom: 31px;
left: 0;
height: 50px;
z-index: -1;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3),
0 6px 0 -3px rgba(255, 255, 255, 0.8),
0 7px 1px -3px rgba(0, 0, 0, 0.3),
0 43px 0 -6px rgba(255, 255, 255, 0.8),
0 44px 2px -6px rgba(0, 0, 0, 0.2);
}
#todo-count {
float: left;
text-align: left;
}
#filters {
margin: 0;
padding: 0;
list-style: none;
position: absolute;
right: 0;
left: 0;
}
#filters li {
display: inline;
}
#filters li a {
color: #83756f;
margin: 2px;
text-decoration: none;
}
#filters li a.selected {
font-weight: bold;
}
#clear-completed {
float: right;
position: relative;
line-height: 20px;
text-decoration: none;
background: rgba(0, 0, 0, 0.1);
font-size: 11px;
padding: 0 10px;
border-radius: 3px;
box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.2);
}
#clear-completed:hover {
background: rgba(0, 0, 0, 0.15);
box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.3);
}
#info {
margin: 65px auto 0;
color: #a6a6a6;
font-size: 12px;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.7);
text-align: center;
}
#info a {
color: inherit;
}
/*
Hack to remove background from Mobile Safari.
Can't use it globally since it destroys checkboxes in Firefox and Opera
*/
@media screen and (-webkit-min-device-pixel-ratio:0) {
#toggle-all,
#todo-list li .toggle {
background: none;
}
#todo-list li .toggle {
height: 40px;
}
#toggle-all {
top: -56px;
left: -15px;
width: 65px;
height: 41px;
-webkit-transform: rotate(90deg);
-ms-transform: rotate(90deg);
transform: rotate(90deg);
-webkit-appearance: none;
appearance: none;
}
}
.hidden {
display: none;
}
hr {
margin: 20px 0;
border: 0;
border-top: 1px dashed #C5C5C5;
border-bottom: 1px dashed #F7F7F7;
}
.learn a {
font-weight: normal;
text-decoration: none;
color: #b83f45;
}
.learn a:hover {
text-decoration: underline;
color: #787e7e;
}
.learn h3,
.learn h4,
.learn h5 {
margin: 10px 0;
font-weight: 500;
line-height: 1.2;
color: #000;
}
.learn h3 {
font-size: 24px;
}
.learn h4 {
font-size: 18px;
}
.learn h5 {
margin-bottom: 0;
font-size: 14px;
}
.learn ul {
padding: 0;
margin: 0 0 30px 25px;
}
.learn li {
line-height: 20px;
}
.learn p {
font-size: 15px;
font-weight: 300;
line-height: 1.3;
margin-top: 0;
margin-bottom: 0;
}
.quote {
border: none;
margin: 20px 0 60px 0;
}
.quote p {
font-style: italic;
}
.quote p:before {
content: '“';
font-size: 50px;
opacity: .15;
position: absolute;
top: -20px;
left: 3px;
}
.quote p:after {
content: '”';
font-size: 50px;
opacity: .15;
position: absolute;
bottom: -42px;
right: 3px;
}
.quote footer {
position: absolute;
bottom: -40px;
right: 0;
}
.quote footer img {
border-radius: 3px;
}
.quote footer a {
margin-left: 5px;
vertical-align: middle;
}
.speech-bubble {
position: relative;
padding: 10px;
background: rgba(0, 0, 0, .04);
border-radius: 5px;
}
.speech-bubble:after {
content: '';
position: absolute;
top: 100%;
right: 30px;
border: 13px solid transparent;
border-top-color: rgba(0, 0, 0, .04);
}
.learn-bar > .learn {
position: absolute;
width: 272px;
top: 8px;
left: -300px;
padding: 10px;
border-radius: 5px;
background-color: rgba(255, 255, 255, .6);
-webkit-transition-property: left;
transition-property: left;
-webkit-transition-duration: 500ms;
transition-duration: 500ms;
}
@media (min-width: 899px) {
.learn-bar {
width: auto;
margin: 0 0 0 300px;
}
.learn-bar > .learn {
left: 8px;
}
.learn-bar #todoapp {
width: 550px;
margin: 130px auto 40px auto;
}
}

View file

@ -0,0 +1,18 @@
.todoitem-enter {
opacity: 0.1;
transition: opacity .2s ease-in;
}
.todoitem-enter.todoitem-enter-active {
opacity: 1;
}
.todoitem-leave {
opacity: 0.8;
transition: opacity 0.2s ease-out;
}
.todoitem-leave.todoitem-leave-active {
opacity: 0.1;
}

View file

@ -0,0 +1,180 @@
(ns todomvc.client
(:require
[reagent.core :as r]
[ajax.core :refer [POST default-interceptors to-interceptor]]
[net.thegeez.browserchannel.client :as browserchannel]
[views.reagent.client.component :refer [view-cursor] :refer-macros [defvc]]
[views.reagent.browserchannel.client :as vr]))
;; Todo MVC - views.reagent example app
;;
;; 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 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 complete-all [v] (POST "/todos/mark-all" {:format :url :params {:done? v}}))
(defn clear-done [] (POST "/todos/delete-all-done"))
;; UI Components
(defn todo-input
[{:keys [title on-save on-stop]}]
(let [val (r/atom title)
stop #(do (reset! val "")
(if on-stop (on-stop)))
save #(let [v (-> @val str clojure.string/trim)]
(if-not (empty? v) (on-save v))
(stop))]
(fn [props]
[:input (merge props
{:type "text" :value @val :on-blur save
:on-change #(reset! val (-> % .-target .-value))
:on-key-down #(case (.-which %)
13 (save)
27 (stop)
nil)})])))
(def todo-edit (with-meta todo-input
{:component-did-mount #(.focus (r/dom-node %))}))
(defn todo-stats
[{:keys [filt active done]}]
(let [props-for (fn [name]
{:class (if (= name @filt) "selected")
:on-click #(reset! filt name)})]
[:div
[:span#todo-count
[:strong active] " " (case active 1 "item" "items") " left"]
[:ul#filters
[:li [:a (props-for :all) "All"]]
[:li [:a (props-for :active) "Active"]]
[:li [:a (props-for :done) "Completed"]]]
(when (pos? done)
[:button#clear-completed {:on-click clear-done}
"Clear completed " done])]))
(defn todo-item
[]
(let [editing (r/atom false)]
(fn [{:keys [id done title]}]
[:li {:class (str (if done "completed ")
(if @editing "editing"))}
[:div.view
[:input.toggle {:type "checkbox" :checked done
:on-change #(toggle id)}]
[:label {:on-double-click #(reset! editing true)} title]
[:button.destroy {:on-click #(delete id)}]]
(when @editing
[todo-edit {:class "edit" :title title
:on-save #(save id %)
: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 views.reagent 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]
(let [filt (r/atom :all)]
(fn []
(let [items (view-cursor :todos)
done (->> @items (filter :done) count)
active (- (count @items) done)]
[:div
[:section#todoapp
[:header#header
[:h1 "todos"]
[todo-input {:id "new-todo"
:placeholder "What needs to be done?"
:on-save add-todo}]]
(when (-> @items count pos?)
[:div
[:section#main
[:input#toggle-all {:type "checkbox" :checked (zero? active)
:on-change #(complete-all (pos? active))}]
[:label {:for "toggle-all"} "Mark all as complete"]
[:ul#todo-list
(for [todo (->> @items
(filter
(case @filt
:active (complement :done)
:done :done
:all identity))
(sort-by :id))]
^{:key (:id todo)} [todo-item todo])]]
[:footer#footer
[todo-stats {:active active :done done :filt filt}]]])]
[:footer#info
[:p "Double-click to edit a todo"]]]))))
;; 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))
;; Page load
(defn ^:export run
[]
; Initialize views.reagent
(vr/init!)
; Initialize BrowserChannel
; NOTE: We are passing in an empty event handler map to connect! only because
; this todo app is not using BrowserChannel for any purpose other then to
; provide client/server messaging for views.reagent. 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 views.reagent.
(browserchannel/connect! {} {:middleware [vr/middleware]})
(r/render-component [todo-app] (.getElementById js/document "app")))

View file

@ -0,0 +1,188 @@
(ns todomvc.server
(:gen-class)
(:require
[compojure.core :refer [routes GET POST]]
[compojure.route :as route]
[ring.middleware.defaults :refer [wrap-defaults site-defaults]]
[ring.util.anti-forgery :refer [anti-forgery-field]]
[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]
[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.browserchannel.server :as vr]))
(def dev? (boolean (env :dev)))
(def db {:classname "org.postgresql.Driver"
:subprotocol "postgresql"
:subname "//localhost/todomvc"
:user "todomvc"
:password "s3cr3t"})
;; View system atom
;;
;; We just declare it, don't need to fill it with anything. The call below to
;; views.reagent.browserchannel.server/init! will take care of it.
(defonce view-system (atom {}))
;; View functions.
;;
;; 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 todos-list
[]
["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.
(def views
[(view :todos db #'todos-list)])
;; 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]
(vexec! view-system db ["INSERT INTO todos (title) VALUES (?)" title])
(response "ok"))
(defn delete-todo!
[id]
(vexec! view-system db ["DELETE FROM todos WHERE id = ?" id])
(response "ok"))
(defn update-todo!
[id title]
(vexec! view-system db ["UPDATE todos SET title = ? WHERE id = ?" title id])
(response "ok"))
(defn toggle-todo!
[id]
; note that a transaction is obviously not necessary here as we could have used
; just a single UPDATE query. however, it is being done this way to demonstrate
; using transactions with vexec!.
(with-view-transaction
view-system
[dt db]
(let [done? (:done (first (jdbc/query dt ["SELECT done FROM todos WHERE id = ?" id])))]
(vexec! view-system dt ["UPDATE todos SET done = ? WHERE id = ?" (not done?) id]))
(response "ok")))
(defn mark-all!
[done?]
(vexec! view-system db ["UPDATE todos SET done = ?" done?])
(response "ok"))
(defn delete-all-done!
[]
(vexec! view-system db ["DELETE FROM todos WHERE done = true"])
(response "ok"))
;; main page html
(defn render-page
[]
(html5
[:head
[: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();")]))
;; Compojure routes and Ring handler
(def app-routes
(routes
; db action routes
(POST "/todos/add" [title] (add-todo! title))
(POST "/todos/delete" [id] (delete-todo! (Integer/parseInt id)))
(POST "/todos/update" [id title] (update-todo! (Integer/parseInt id) title))
(POST "/todos/toggle" [id] (toggle-todo! (Integer/parseInt id)))
(POST "/todos/mark-all" [done?] (mark-all! (Boolean/parseBoolean done?)))
(POST "/todos/delete-all-done" [] (delete-all-done!))
; main page
(GET "/" [] (render-page))
(route/resources "/")
(route/not-found "not found")))
; NOTE: We are passing in an empty event handler map to wrap-browserchannel only
; because this todo app is not using BrowserChannel for any purpose other
; then to provide client/server messaging for views.reagent. 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 views.reagent.
(def handler
(-> app-routes
(wrap-defaults (assoc-in site-defaults [:security :anti-forgery] (not dev?)))
(wrap-browserchannel {} {:middleware [(vr/->middleware view-system)]})
(wrap-immutant-async-adapter)))
;; Web server startup & main
(defn run-server
[]
; views.reagent.browserchannel.server/init! takes care of initialization of views
; and views.reagent at the same time. As a result, we do not need to also call
; views.core/init! anywhere. The same arguments and options you are able to pass to
; views.core/init! can also be passed in here and they will be forwarded along, as
; this function is intended to be a drop-in replacement for views.core/init!.
;
; 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!.
(vr/init! view-system {:views views})
(immutant/run handler {:port 8080}))
(defn -main
[& args]
(run-server))

View file

@ -17,10 +17,10 @@ you will need to first clone the following repositories and manually
install the libraries via `lein install`: install the libraries via `lein install`:
* [views](https://github.com/gered/views) * [views](https://github.com/gered/views)
* [views-sql](https://github.com/gered/views-sql) * [views.sql](https://github.com/gered/views.sql)
* [views.reagent](https://github.com/gered/views.reagent) * [views.reagent](https://github.com/gered/views.reagent)
As well, you can install [views-honeysql](https://github.com/gered/views-honeysql) 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 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. this example app does not use it so it's not required.

View file

@ -8,12 +8,11 @@
[org.clojure/java.jdbc "0.6.1"] [org.clojure/java.jdbc "0.6.1"]
[org.postgresql/postgresql "9.4.1208.jre7"] [org.postgresql/postgresql "9.4.1208.jre7"]
[gered/clj-browserchannel "0.3.2"] [com.taoensso/sente "1.8.1"]
[gered/clj-browserchannel-immutant-adapter "0.0.3"]
[gered/views "1.5-SNAPSHOT"] [gered/views "1.5-SNAPSHOT"]
[gered/views.sql "0.1.0-SNAPSHOT"] [gered/views.sql "0.1.0-SNAPSHOT"]
[gered/views.reagent "0.2.0-SNAPSHOT"] [gered/views.reagent "0.2.0-SNAPSHOT"]
[gered/views.reagent.browserchannel "0.1.0-SNAPSHOT"] [gered/views.reagent.sente "0.1.0-SNAPSHOT"]
[hiccup "1.0.5"] [hiccup "1.0.5"]
[reagent "0.6.0-alpha2"] [reagent "0.6.0-alpha2"]

View file

@ -2,9 +2,9 @@
(:require (:require
[reagent.core :as r] [reagent.core :as r]
[ajax.core :refer [POST default-interceptors to-interceptor]] [ajax.core :refer [POST default-interceptors to-interceptor]]
[net.thegeez.browserchannel.client :as browserchannel] [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.browserchannel.client :as vr])) [views.reagent.sente.client :as vr]))
;; Todo MVC - Reagent Implementation ;; Todo MVC - Reagent Implementation
;; ;;
@ -19,6 +19,15 @@
;; Sente socket
;;
;; This just holds the socket map returned by sente's make-channel-socket!
;; so we can refer to it and pass it around as needed.
(defonce sente-socket (atom {}))
;; AJAX operations ;; 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}}))
@ -162,19 +171,45 @@
;; Sente event/message handler
;;
;; Note that if you're only using Sente to make use of views.reagent in your app
;; and aren't otherwise using it for any other client/server messaging, you can
;; set :use-default-sente-router? to true in the options passed to
;; views.reagent.sente.client/init! (called in run below). Then you will not
;; need to provide a handler like this to start-chsk-router! as one will be
;; provided automatically.
(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)
(= :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
))))
;; Page load ;; Page load
(defn ^:export run (defn ^:export run
[] []
; Configure views.reagent and then BrowserChannel. ; Sente setup. create the socket, storing it in an atom and set up a event
(vr/init!) ; handler using sente's own message router functionality.
(reset! sente-socket (sente/make-channel-socket! "/chsk" {}))
; NOTE: We are passing in an empty map for the BrowserChannel event handlers only ; set up a handler for sente events
; because this todo app is not using BrowserChannel for any purpose other (sente/start-chsk-router! (:ch-recv @sente-socket) sente-event-msg-handler)
; then to provide client/server messaging for views.reagent. If we
; wanted to use it for client/server messaging in our application as well, ; Configure views.reagent for use with Sente.
; we could pass in any event handlers we want here and it would not intefere (vr/init! @sente-socket {})
; with views.reagent.
(browserchannel/connect! {} {:middleware [vr/middleware]})
(r/render-component [todo-app] (.getElementById js/document "app"))) (r/render-component [todo-app] (.getElementById js/document "app")))

View file

@ -6,8 +6,8 @@
[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.anti-forgery :refer [anti-forgery-field]]
[ring.util.response :refer [response]] [ring.util.response :refer [response]]
[net.thegeez.browserchannel.server :refer [wrap-browserchannel]] [taoensso.sente :as sente]
[net.thegeez.browserchannel.immutant-async-adapter :refer [wrap-immutant-async-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]]
@ -15,7 +15,7 @@
[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.browserchannel.server :as vr])) [views.reagent.sente.server :as vr]))
(def dev? (boolean (env :dev))) (def dev? (boolean (env :dev)))
@ -27,10 +27,19 @@
;; Sente socket
;;
;; This just holds the socket map returned by sente's make-channel-socket!
;; so we can refer to it and pass it around as needed.
(defonce sente-socket (atom {}))
;; View system atom ;; View system atom
;; ;;
;; We just declare it, don't need to fill it with anything. The call below to ;; We just declare it, don't need to fill it with anything. The call below to
;; views.reagent.browserchannel.server/init! will take care of it. ;; views.reagent.sente.server/init! will take care of initializing it.
(defonce view-system (atom {})) (defonce view-system (atom {}))
@ -44,7 +53,7 @@
;; A view function's return value requirement depends on what views IView ;; A view function's return value requirement depends on what views IView
;; implementation is being used. ;; implementation is being used.
;; ;;
;; This example app is using views-sql, so view templates should return a SQL SELECT ;; 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 ;; 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 actual SQL query and is followed by any number of parameters to be used in
;; the query. ;; the query.
@ -146,6 +155,10 @@
(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!))
; sente routes
(GET "/chsk" request ((:ajax-get-or-ws-handshake-fn @sente-socket) request))
(POST "/chsk" request ((:ajax-post-fn @sente-socket) request))
; main page ; main page
(GET "/" [] (render-page)) (GET "/" [] (render-page))
@ -154,15 +167,29 @@
(def handler (def handler
(-> app-routes (-> app-routes
(wrap-defaults (assoc-in site-defaults [:security :anti-forgery] true #_(not dev?))) (wrap-defaults (assoc-in site-defaults [:security :anti-forgery] (not dev?)))))
; NOTE: We are passing in an empty map for the BrowserChannel event handlers only
; because this todo app is not using BrowserChannel for any purpose other
; then to provide client/server messaging for views.reagent. If we
; wanted to use it for client/server messaging in our application as well, ;; Sente event/message handler
; we could pass in any event handlers we want here and it would not intefere ;;
; with views.reagent. ;; Note that if you're only using Sente to make use of views.reagent in your app
(wrap-browserchannel {} {:middleware [(vr/->middleware view-system)]}) ;; and aren't otherwise using it for any other client/server messaging, you can
(wrap-immutant-async-adapter))) ;; set :use-default-sente-router? to true in the options passed to
;; views.reagent.sente.server/init! (called in run-server below). Then you will
;; not need to provide a handler like this to start-chsk-router! as one will be
;; provided automatically.
(defn sente-event-msg-handler
[{:keys [event id uid client-id] :as ev}]
(if (= id :chsk/uidport-close)
(vr/on-close! view-system ev)
(when-not (vr/on-receive! view-system 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
)))
@ -170,14 +197,30 @@
(defn run-server (defn run-server
[] []
; init! takes care of initialization of views and views.reagent at the same ; sente setup. create the socket, storing it in an atom and set up a event
; time. As a result, we do not need to also call views.core/init! anywhere. The ; handler using sente's own message router functionality.
; same arguments and options you are able to pass to views.core/init! can also be ; in this example app we are setting up sente user-id's to just be the
; passed in here and they will be forwarded along, as this function is intended to be ; client-id, but you can obviously set this up however you wish.
; a drop-in replacement for views.core/init!. (reset! sente-socket
; if you need to shutdown the views system (e.g. if you're using something like (sente/make-channel-socket!
sente-web-server-adapter
{:user-id-fn (fn [request] (get-in request [:params :client-id]))}))
; set up a handler for sente events
(sente/start-chsk-router! (:ch-recv @sente-socket) sente-event-msg-handler)
; views.reagent.sente.server/init! takes care of two things for us:
;
; 1. initialization of the base views system
; 2. customization of views system config for use with sente
;
; As a result, we do not also need to call views.core/init! anywhere, as
; this performs the exact same function and also accepts the same arguments
; and options -- see the docs for views.core/init! for more info.
;
; 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!. ; Component or Mount), you can just call views.core/shutdown!.
(vr/init! view-system {:views views}) (vr/init! view-system @sente-socket {:views views})
(immutant/run handler {:port 8080})) (immutant/run handler {:port 8080}))