Commit 2f45aabc authored by Alex Sarmanov's avatar Alex Sarmanov

init

parents
.cljs_rhino_repl
target
resources/public/js
figwheel_server.log
.lein-*
.nrepl-port
pom.xml
pom.xml.asc
# Skeleton CLJS+Redux like app
Has:
- http-kit server
- compojure router
- clojurescript app
- rum/react
- websocket endpoint
- transit serialization
- figwheel development server
- piggieback cljs repl
- beta reduxDevToolsConnection
## Dev setup
```sh
lein figwheel &
open http://localhost:8080/
```
Some js bridge to devTools Redux is in resources/public/devTools/devTools.js
need to be compiled with convert.sh (babel, node)
P.S. It's not real redux, it is clojurescript atom mirrored to reduxDevTools internal js state object
It is enought for me at the moment
#
![some info](http://gitlab.xet.ru:9999/publicpr/clojurescript-redux/raw/master/resources/public/gif/sample.gif)
## Prod setup
```sh
lein package
java -jar target/skeleton.jar &
open http://localhost:8080/
```
Here `lein package` is just an alias for `lein do cljsbuild once advanced, uberjar`.
Based on https://github.com/tonsky/cljs-skeleton and https://github.com/jellea/redux-cljs
lein figwheel
\ No newline at end of file
(defproject cljs-bmain "0.1.0-SNAPSHOT"
:dependencies [
[org.clojure/clojure "1.8.0"]
[org.clojure/clojurescript "1.9.225"]
[rum "0.10.6"]
[http-kit "2.2.0"]
[compojure "1.5.1" :exclusions [commons-codec]]
[com.cognitect/transit-clj "0.8.288"]
[com.cognitect/transit-cljs "0.8.239"]
]
:plugins [
[lein-cljsbuild "1.1.3"]
[lein-figwheel "0.5.4-7"]
]
:aliases { "package" ["do"
"cljsbuild" "once" "advanced,"
"uberjar"] }
:aot [ bmain.server ]
:uberjar-name "bmain.jar"
:uberjar-exclusions [#"public/js/out"]
:main bmain.server
:figwheel { :ring-handler "bmain.server/app"
:css-dirs ["resources/public"]
:server-port 8080
:repl true }
:profiles {:dev {:dependencies [[com.cemerick/piggieback "0.2.1"]
[org.clojure/tools.nrepl "0.2.12"]]
:repl-options {:nrepl-middleware [cemerick.piggieback/wrap-cljs-repl]}}}
:cljsbuild {
:builds [
{ :id "none"
:source-paths ["src"]
:figwheel { :on-jsload "bmain.app/refresh" }
:compiler { :optimizations :none
:main bmain.app
:asset-path "/js/out"
:output-to "resources/public/js/main.js"
:output-dir "resources/public/js/out"
:source-map true
:compiler-stats true } }
{ :id "advanced"
:source-paths ["src"]
:compiler { :optimizations :advanced
:main bmain.app
:output-to "resources/public/js/main.js"
:compiler-stats true
:pretty-print false
:pseudo-names false } }
]}
)
{
"presets": [
"es2015",
"stage-0"
]
}
\ No newline at end of file
node_modules
\ No newline at end of file
#!/bin/sh
#first install this to convert es6 to es5
#npm install --save-dev babel-cli babel-preset-es2015 babel-preset-stage-0
./node_modules/.bin/babel ./devTools.js --out-file ./devTools-es5.js
'use strict';
/*
Try to prototype just mirroring state to and from reduxDevTools,
not real redux, but mirror clorurescript global atom
*/
var reducer = function reducer() {
var state = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];
var action = arguments[1];
// all reducers already calculated in cljs, only result as mirror state
return action.reducerResult || {};
};
var withDevTools = typeof window !== 'undefined' && window.devToolsExtension;
if (withDevTools) {
window.store = window.devToolsExtension(reducer);
// window.store.subscribe(() => {
// call to cljs @TODO
// need to get all history state from reduxDevTools, how can i get them?
// and send only if action come from devToolsExtension, how can i filter them?
// redux.core.restorestate(window.store.getState());
// });
}
var dispatch = function dispatch(action) {
if (withDevTools) {
window.store.dispatch(action);
}
};
/*
Try to prototype just mirroring state to and from reduxDevTools,
not real redux, but mirror clorurescript global atom
*/
const reducer = (state = {}, action) => {
// all reducers already calculated in cljs, only result as mirror state
return action.reducerResult || {};
}
const withDevTools = (
typeof window !== 'undefined' && window.devToolsExtension
);
if(withDevTools) {
window.store = window.devToolsExtension(reducer);
// window.store.subscribe(() => {
// call to cljs @TODO
// need to get all history state from reduxDevTools, how can i get them?
// and send only if action come from devToolsExtension, how can i filter them?
// redux.core.restorestate(window.store.getState());
// });
}
const dispatch = (action) => {
if (withDevTools) {
window.store.dispatch(action);
}
}
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html;charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" />
<title>Skeleton app</title>
<link href="/style.css" rel="stylesheet" type="text/css" />
</head>
<body>
<div id="app"></div>
<script src="/devTools/devTools-es5.js" type="text/javascript"></script>
<script src="/js/main.js" type="text/javascript"></script>
<script type="text/javascript">
bmain.app.refresh();
</script>
</body>
</html>
body {
text-align: center;
}
#app {
font: 14px Georgia, serif;
}
.cursor {
cursor: pointer;
cursor: hand;
}
.hover:hover {
background-color:gray;
color: #fff;
}
button {
font-size:18px;
padding:15px;
margin:5px;
}
.state {
color: green;
}
.history-component {
border: 1px dotted gray;
}
\ No newline at end of file
(ns bmain.app
(:require
[rum.core :as rum]
[redux.core :as r]
[utils.sys :as sys]
[cognitect.transit :as t]
[clojure.data :as cldata]))
(enable-console-print!)
(defn read-transit-str [s]
(t/read (t/reader :json) s))
(defn write-transit-str [o]
(t/write (t/writer :json ) o))
(declare send!)
;; server connection @TODO need to put on dispatcher return action
(defonce socket
(doto (js/WebSocket. (str "ws://" js/location.host "/api/websocket"))
(aset "onmessage"
(fn [payload]
"Recieve server socket message"
(let [message (read-transit-str (.-data payload))]
;; @TODO too fuzzy now on refresh
;; (r/dispatch! {:type :socket-on-message :message message})
)))
(aset "onopen"
#(send! "connected"))))
;; server connection @TODO need to put on dispatcher return action
(defn send! [message]
"Send message to socket"
(when (== 1 (.-readyState socket)) ;; WS_OPEN
(.send socket (write-transit-str message))))
(rum/defc state-history < rum/reactive []
[:div.history-component
"state-history component"
(for [x (sys/indexed-vector @r/!actions-history)]
(let [index (nth x 0)]
[:div.cursor.hover {:key (str index)}
(str index " -> action -> ")
(str (nth x 1) " -> diff -> ")
(str (sys/diff-state @r/!state-history index))
]))])
(rum/defc app < rum/reactive []
"Root rum/react component"
(let [state (rum/react r/!state)]
[:div "[" (:count state) "] " (pr-str (:message state))]
[:div
[:div (state-history)]
[:pre.state "current state: " (str @r/!state)]
;; [:pre "actions: " (str @r/!actions-history)]
;; [:pre "state-history: " (str @r/!state-history)]
[:button {:on-click #(r/dispatch! {:type :some-greeting :name "YourName"})} "Hello"]
[:button {:on-click #(r/dispatch! {:type :math-increment :num 1})} "+1"]
[:button {:on-click #(r/dispatch! {:type :math-increment :num 10})} "+10"]
[:button {:on-click #(r/dispatch! {:type :math-decrement :num 1})} "-1"]
[:button {:on-click #(r/dispatch! {:type :math-decrement :num 10})} "-10"]
[:button {:on-click #(r/dispatch! {:type :HelloWorld})} "Error"]]
))
(defn ^:export refresh []
"Remount rum/react application to root node"
(send! "refreshed")
(rum/mount (app) (js/document.querySelector "#app")))
(ns bmain.server
(:require
[compojure.core :as compojure]
[compojure.route :as route]
[org.httpkit.server :as httpkit]
[ring.util.response :as response]
[cognitect.transit :as t])
(:gen-class))
(defn read-transit-str [s]
(-> s
(.getBytes "UTF-8")
(java.io.ByteArrayInputStream.)
(t/reader :json)
(t/read)))
(defn write-transit-str [o]
(let [os (java.io.ByteArrayOutputStream.)]
(t/write (t/writer os :json) o)
(String. (.toByteArray os) "UTF-8")))
(compojure/defroutes app
(compojure/GET "/api/websocket" [:as req]
(httpkit/with-channel req chan
(println "Connected")
(httpkit/on-close chan
(fn [status]
(println "Disconnected")))
(httpkit/on-receive chan
(fn [payload]
(let [message (read-transit-str payload)]
(println "Recieved:" message)
(httpkit/send! chan (write-transit-str ["pong" message])))))))
(compojure/GET "/" [] (response/resource-response "public/index.html"))
(route/resources "/" {:root "public"}))
(defn -main [& args]
(println "Starting server at port 8080")
(httpkit/run-server #'app {:port 8080}))
(ns reducers.math
(:require [redux.reducer :refer [Action]]))
(defmethod Action :math-increment [{:keys [num]} state]
(assoc-in state [:math :counter] (+ (get-in state [:math :counter]) num)))
(defmethod Action :math-decrement [{:keys [num]} state]
(assoc-in state [:math :counter] (- (get-in state [:math :counter]) num)))
(ns reducers.socket
(:require [redux.reducer :refer [Action]]))
(defmethod Action :socket-on-message [answer]
(assoc-in state [:socket :message] (:message answer)))
(ns reducers.text
(:require [redux.reducer :refer [Action]]))
(defmethod Action :some-greeting [{:keys [name]} state]
(assoc-in state [:text :some-greeting] (gensym name)))
(ns redux.core
(:require [rum.core :as r]
[redux.reducer :refer [Action]]
[reducers.socket]
[reducers.math]
[reducers.text]
))
(def dev-tools js/devToolsExtension)
(enable-console-print!)
(def initial-state {:math {:counter 0}})
(defonce !actions-history (atom []))
(defonce !state-history (atom []))
(defonce !state (atom initial-state))
(defn dispatch! [action]
"Save actions to actions history"
(swap! !actions-history conj action))
(add-watch !actions-history :reduce
(fn [_ _ _ new-state]
"Call main reducer thru defmulti"
(let [new-action (last new-state)]
(swap! !state #(Action new-action %)))))
(add-watch !state :watch-change
(fn [key a old-val new-val]
"This function only for send changed state to reduxDevTools"
(swap! !state-history conj new-val)
(when dev-tools
(let [reducer-result (hash-map :reducerResult @!state)]
(def result (conj (select-keys (peek @!actions-history) [:type]) reducer-result))
(js/dispatch (clj->js result))))))
(defn ^:export restorestate
"restore state from reduxDevTools, has problem with history ordering now
this is howto export name function in cljs to js, call this function as redux.core.restorestate(store);"
[state]
;(js/console.warn state)
(when (contains? (js->clj state) :reducerResult)
;(js/console.info state)
))
(defmethod Action :default [{:keys [type] :as action-data} state]
"Fallback Action, gives an informative warning in console when triggering undefined Action. And return state as is."
(js/console.warn (str "Action " type " with data " (str action-data) " not defined."))
state)
(ns redux.reducer)
;; call reducer
(defmulti Action (fn [action state]
"Call reducer here from action "
(:type action)))
(ns redux.socket
(:require [redux.reducer :refer [Action]]))
(enable-console-print!)
;; socket status @TODO
;; (defmethod Action :socket-on-message [answer]
;; (assoc-in state [:socket :message] (:message answer)))
(ns utils.sys
(:require [clojure.data :as cldata]))
(defn indexed-vector [vec]
"Return [a b] vector with indexes [[0 a] [1 b] ...]"
(map-indexed vector vec))
(defn diff-state [state index1 & [index2]]
"Compare two states in state-history by indexes, if index2 is not provided, will compare with previous index"
(def indexed-state (indexed-vector state))
(let [index2 (or index2 (- index1 1))]
(if (neg? index2)
(last (nth indexed-state 0))
(first (cldata/diff
(last (nth indexed-state index1))
(last (nth indexed-state index2)))))))
lein ancient upgrade :check-clojure :interactive
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment