diff --git a/project.clj b/project.clj index c6b83eb..80aabc3 100644 --- a/project.clj +++ b/project.clj @@ -1,13 +1,13 @@ (defproject omelette "0.0.0" - :description "Example of mirrored server/client rendering and routing using Om, Sente, Secretary, and the Nashorn JavaScript engine." + :description "Example of mirrored server/client rendering and routing using Om, Sente, and the Nashorn JavaScript engine." :license {:name "Eclipse Public License" :url "http://www.eclipse.org/legal/epl-v10.html" :distribution :repo :comments "Same as Clojure"} - :main omelette.handler + :main omelette.main :global-vars {*warn-on-reflection* true} @@ -15,22 +15,21 @@ :resource-paths ["resources" "target/resources"] - :dependencies [[ankha "0.1.2"] - [com.cemerick/url "0.1.1"] + :dependencies [[ankha "0.1.3"] + [com.stuartsierra/component "0.2.1"] [com.taoensso/encore "1.6.0"] [com.taoensso/sente "0.14.1"] [compojure "1.1.8"] [hiccup "1.0.5"] [http-kit "2.1.18"] - [lein-light-nrepl "0.0.18"] [markdown-clj "0.9.44"] - [om "0.6.2"] + [om "0.6.4"] [org.clojure/clojure "1.6.0"] [org.clojure/clojurescript "0.0-2227"] [org.clojure/core.async "0.1.298.0-2a82a1-alpha"] [org.clojure/core.match "0.2.1"] - [org.clojure/tools.nrepl "0.2.3"] - [ring "1.2.2"] + [org.clojure/tools.namespace "0.2.4"] + [ring "1.3.0"] [ring/ring-anti-forgery "0.3.2"] [sablono "0.2.17"]] @@ -38,14 +37,14 @@ [lein-cljsbuild "1.0.3"] [lein-pdo "0.1.1"]] - :hooks [cljx.hooks] + :hooks [cljx.hooks leiningen.cljsbuild] :cljx {:builds [{:source-paths ["src"], :output-path "target/src", :rules :clj} {:source-paths ["src"], :output-path "target/src", :rules :cljs}]} :cljsbuild {:builds [{:source-paths ["src" "target/src"] :compiler { -;; :preamble ["react/react.js"] + :preamble ["react/react.min.js"] :output-to "target/resources/public/assets/scripts/main.js" :output-dir "target/resources/public/assets/scripts" :source-map "target/resources/public/assets/scripts/main.js.map" diff --git a/resources/about.md b/resources/about.md index 23bcfe8..ed4d010 100644 --- a/resources/about.md +++ b/resources/about.md @@ -1,4 +1,4 @@ -Zombie ipsum reversus ab viral inferno, nam rick grimes malum cerebro. De carne lumbering animata corpora quaeritis. Summus brains sit​​, morbo vel maleficia? De apocalypsi gorger omero undead survivor dictum mauris. Hi mindless mortuis soulless creaturas, imo evil stalking monstra adventus resi dentevil vultus comedat cerebella viventium. Qui animated corpse, cricket bat max brucks terribilem incessu zomby. The voodoo sacerdos flesh eater, suscitat mortuos comedere carnem virus. Zonbi tattered for solum oculi eorum defunctis go lum cerebro. Nescio brains an Undead zombies. Sicut malus putrid voodoo horror. Nigh tofth eliv ingdead. +Zombie ipsum reversus ab viral inferno, nam rick grimes malum cerebro. De carne lumbering animata corpora quaeritis. Summus brains sit, morbo vel maleficia? De apocalypsi gorger omero undead survivor dictum mauris. Hi mindless mortuis soulless creaturas, imo evil stalking monstra adventus resi dentevil vultus comedat cerebella viventium. Qui animated corpse, cricket bat max brucks terribilem incessu zomby. The voodoo sacerdos flesh eater, suscitat mortuos comedere carnem virus. Zonbi tattered for solum oculi eorum defunctis go lum cerebro. Nescio brains an Undead zombies. Sicut malus putrid voodoo horror. Nigh tofth eliv ingdead. Cum horribilem walking dead resurgere de crazed sepulcris creaturis, zombie sicut de grave feeding iride et serpens. Pestilentia, shaun ofthe dead scythe animated corpses ipsa screams. Pestilentia est plague haec decaying ambulabat mortuos. Sicut zeder apathetic malus voodoo. Aenean a dolor plan et terror soulless vulnerum contagium accedunt, mortui iam vivam unlife. Qui tardius moveri, brid eof reanimator sed in magna copia sint terribiles undeath legionis. Alii missing oculis aliorum sicut serpere crabs nostram. Putridi braindead odores kill and infect, aere implent left four dead. @@ -7,3 +7,4 @@ Lucio fulci tremor est dark vivos magna. Expansis creepy arm yof darkness ulnis In Craven omni memoria patriae zombieland clairvius narcisse religionis sunt diri undead historiarum. Golums, zombies unrelenting et Raimi fascinati beheading. Maleficia! Vel cemetery man a modern bursting eyeballs perhsaps morbi. A terrenti flesh contagium. Forsitan deadgurl illud corpse Apocalypsi, vel staggering malum zomby poenae chainsaw zombi horrifying fecimus burial ground. Indeflexus shotgun coup de poudre monstra per plateas currere. Fit de decay nostra carne undead. Poenitentiam violent zom biehig hway agite RE:dead pœnitentiam! Vivens mortua sunt apud nos night of the living dead. Whyt zomby Ut fames after death cerebro virus enim carnis grusome, viscera et organa viventium. Sicut spargit virus ad impetum, qui supersumus flesh eating. Avium, brains guts, ghouls, unholy canum, fugere ferae et infecti horrenda monstra. Videmus twenty-eight deformis pale, horrenda daemonum. Panduntur brains portae rotting inferi. Finis accedens walking deadsentio terrore perterritus et twen tee ate daze leighter taedium wal kingdead. The horror, monstra epidemic significant finem. Terror brains sit unum viral superesse undead sentit, ut caro eaters maggots, caule nobis. + diff --git a/src/omelette/handler.clj b/src/omelette/handler.clj deleted file mode 100644 index 4cd59bb..0000000 --- a/src/omelette/handler.clj +++ /dev/null @@ -1,85 +0,0 @@ -(ns omelette.handler - (:gen-class) - (:require [clojure.tools.nrepl.server :as nrepl] - [compojure.core :refer [GET POST ANY defroutes routes]] - [compojure.handler :as handler] - [compojure.route ] - [hiccup.page :refer [html5]] - [lighttable.nrepl.handler :as lt] - [omelette.route :as route] - [omelette.render :as render] - [org.httpkit.server :refer [run-server]] - [ring.middleware.anti-forgery :as ring-anti-forgery] - [ns-tracker.core :refer [ns-tracker]])) - -(let [stop-router! (atom nil) - reset-router! (fn [] - (@stop-router!) - (reset! stop-router! (route/start!)))] - (defn wrap-reload - "Reload namespaces of modified files before the request is passed to the - supplied handler. - - Takes the following options: - :dirs - A list of directories that contain the source files. - Defaults to [\"src\"]." - [handler & [options]] - - (reset! stop-router! (route/start!)) - - (let [source-dirs (:dirs options ["src" "target/src"]) - modified-namespaces (ns-tracker source-dirs)] - (fn [request] - (doseq [ns-sym (modified-namespaces)] - (require ns-sym :reload)) -;; (require '[omelette.route :as route] :reload) - (reset-router!) - (handler request))))) - -(defn page-handler [req] - (let [{:keys [status title body state]} (render/response req)] - {:status status - :session (assoc (:session req) :uid (or (-> req :session :uid) - (java.util.UUID/randomUUID))) - :body (html5 - [:head - [:meta {:charset "utf-8"}] - [:meta {:http-equiv "X-UA-Compatible" :content "IE=edge,chrome=1"}] - [:title title] - [:meta {:name "viewport" :content "width=device-width"}]] - [:body - [:noscript "If you're seeing this then you're probably a search engine."] - [:div#omelette-app body] - [:script#omelette-state {:type "application/edn"} state] - [:div#ankha {:style "position:absolute;top:100px;right:100px;"}] - [:script {:type "text/javascript" :src "//cdnjs.cloudflare.com/ajax/libs/react/0.10.0/react.js"}] - [:script {:type "text/javascript" :src "/assets/scripts/main.js"}]])})) - -(defroutes app-handler - (-> - (routes - (GET "/chsk" req (route/ring-ajax-get-or-ws-handshake req)) - (POST "/chsk" req (route/ring-ajax-post req)) - (GET "/" req (page-handler req)) - (GET "/about" req (page-handler req)) - (GET "/search" req (page-handler req)) - (GET "/search/*/*" req (page-handler req)) - (compojure.route/resources "/") - (compojure.route/not-found "Not found") -;; This catches everything. How do we just catch routes that aren't resources or /chsk? -;; (GET "*" req (page-handler req)) -;; (route/not-found "Not Found") -) - (ring-anti-forgery/wrap-anti-forgery - {:read-token (fn [req] (-> req :params :csrf-token))}))) - -(defn -main [& args] - (let [handler (-> #'app-handler handler/site (wrap-reload {:dirs ["src" "target/src"]})) - server (run-server handler {:port 0}) - uri (format "http://localhost:%s/" (-> server meta :local-port)) - nrepl-server (nrepl/start-server :handler (nrepl/default-handler #'lt/lighttable-ops)) - nrepl-uri (format "http://localhost:%s/" (:port nrepl-server))] - (println "Server running at " uri) - (println "nrepl server running " nrepl-uri) - (.browse (java.awt.Desktop/getDesktop) - (java.net.URI. uri)))) diff --git a/src/omelette/main.clj b/src/omelette/main.clj new file mode 100644 index 0000000..3babe24 --- /dev/null +++ b/src/omelette/main.clj @@ -0,0 +1,23 @@ +(ns omelette.main + (:gen-class) + (:require [com.stuartsierra.component :as component] + [omelette.route :as route] + [omelette.serve :as serve])) + +(def system + (component/system-map + :router (route/router) + :server (component/using + (serve/server) + [:router]))) + +(defn browse [system] + (->> (get-in system [:server :port]) + (str "http://localhost:") + java.net.URI. + (.browse (java.awt.Desktop/getDesktop)))) + +(defn -main [& _] + (-> system + component/start + browse)) diff --git a/src/omelette/render.clj b/src/omelette/render.clj index 80624a9..07f2d04 100644 --- a/src/omelette/render.clj +++ b/src/omelette/render.clj @@ -1,30 +1,29 @@ (ns omelette.render - (require [clojure.java.io :as io] - [omelette.route :as route]) + (:require [clojure.java.io :as io] + [hiccup.page :refer [html5]]) (:import [javax.script Invocable ScriptEngineManager])) -(let [js (doto (.getEngineByName (ScriptEngineManager.) "nashorn") - ; React requires either "window" or "global" to be defined. - (.eval "var global = this") - ; Rendering to string errors with some components in 0.9.0. - ; Om is waiting for 0.11.0, though I don't know why. - (.eval ^String (slurp "http://cdnjs.cloudflare.com/ajax/libs/react/0.10.0/react.min.js")) - (.eval (-> "public/assets/scripts/main.js" io/resource io/reader)) - ; Is there a way to eval it without using closure compiler optimizations? - ; The compile speed is pretty slow on whitespace, about 6-10 seconds - ) - view (.eval js "omelette.view")] - (defn edn->html [edn] - (.invokeMethod ^Invocable js view "render_to_string" (-> edn list object-array)))) +(defn renderer [] + (let [js (doto (.getEngineByName (ScriptEngineManager.) "nashorn") + (.eval "var global = this") ; React requires either "window" or "global" to be defined. + (.eval (-> "public/assets/scripts/main.js" io/resource io/reader))) + view (.eval js "omelette.view")] + (fn render + [title state-edn] + (html5 + [:head + [:meta {:charset "utf-8"}] + [:meta {:http-equiv "X-UA-Compatible" :content "IE=edge,chrome=1"}] + [:meta {:name "viewport" :content "width=device-width"}] + [:title (str title " | Omelette")]] + [:body + [:noscript "If you're seeing this then you're probably a search engine."] + [:div#omelette-app (.invokeMethod ^Invocable js view "render_to_string" (-> state-edn list object-array))] + [:script {:type "text/javascript" :src "/assets/scripts/main.js"}] + [:script#omelette-state {:type "application/edn"} state-edn] + [:script {:type "text/javascript"} "omelette.view.init('omelette-state')"] + ])))) -(defn response [req] - (let [state (route/handler {:event (-> req :uri route/path->state), :ring-req req}) - state-edn (pr-str state)] - {:status (if (-> state first name (= "not-found")) - 404 - 200) - :title (route/state->title state) - :body (edn->html state-edn) - :state state-edn})) +;; ((renderer) "foo" (pr-str [:omelette.page/not-found {}])) diff --git a/src/omelette/route.cljx b/src/omelette/route.cljx index 0936e39..7e89b6d 100644 --- a/src/omelette/route.cljx +++ b/src/omelette/route.cljx @@ -1,13 +1,24 @@ (ns omelette.route - (:require [cemerick.url :as url] - [clojure.string :as str] + (:require [clojure.string :as str] + [taoensso.encore :as encore] + [taoensso.sente :as sente] #+clj [clojure.core.match :refer [match]] - #+cljs [cljs.core.match] + #+clj [com.stuartsierra.component :as component] + #+clj [compojure.core :as compojure] + #+clj [compojure.route] #+clj [omelette.data :as data] - [taoensso.sente :as sente] - [taoensso.encore :as encore]) + #+clj [omelette.render :as render] + #+cljs [cljs.core.async :as csp] + #+cljs [cljs.core.match] + #+cljs [clojure.set :refer [rename-keys]] + #+cljs [goog.events] + #+cljs [om.core :as om :include-macros true]) #+cljs - (:require-macros [cljs.core.match.macros :refer [match]])) + (:require-macros [cljs.core.async.macros :as csp] + [cljs.core.match.macros :refer [match]]) + #+cljs + (:import goog.history.EventType + goog.history.Html5History)) (defn- encode-search-options [opts] (->> [:prefix :infix :postfix] @@ -21,16 +32,14 @@ (map keyword) set)) -(defn- ^:boolean valid-options-str? [options] - (-> options - #{"prefix" - "infix" - "postfix" - "prefix-infix" - "infix-postfix" - "prefix-postfix" - "prefix-infix-postfix"} - boolean)) +(def ^:private valid-options-str? + #{"prefix" + "infix" + "postfix" + "prefix-infix" + "infix-postfix" + "prefix-postfix" + "prefix-infix-postfix"}) (defn- search->state [query options] (if (valid-options-str? options) @@ -40,9 +49,7 @@ (defn path->state [path] (match - (filterv (complement str/blank?) (-> path - str/lower-case - (str/split #"/"))) + (filterv (complement str/blank?) (-> path str/lower-case (str/split #"/"))) [] (search->state "omelette" "prefix-infix-postfix") ["search"] (search->state "omelette" "prefix-infix-postfix") ["search" options query] (search->state query options) @@ -71,70 +78,136 @@ 3 (encore/format "%s, %s, or %s" a b c))] (encore/format "words that %s \"%s\"" opts-str - (url/url-encode query))) + query)) (->> (str/split page #"-") (map str/capitalize) - (str/join " ") - )))) - + (str/join " "))))) #+clj -(let [{:keys [ch-recv send-fn ajax-post-fn ajax-get-or-ws-handshake-fn connected-uids]} - (sente/make-channel-socket! {})] - (def ring-ajax-post ajax-post-fn) - (def ring-ajax-get-or-ws-handshake ajax-get-or-ws-handshake-fn) - (def recv ch-recv) ; ChannelSocket's receive channel - (def send! send-fn) ; ChannelSocket's send API fn - (def connected-uids connected-uids) ; Watchable, read-only atom - ) - -#+cljs -(when (exists? js/window) - (let [{:keys [chsk ch-recv send-fn state]} - (sente/make-channel-socket! "/chsk" ; Note the same URL as before - {})] - (def chsk chsk) - (def recv ch-recv) ; ChannelSocket's receive channel - (def send! send-fn) ; ChannelSocket's send API fn - (def state state) ; Watchable, read-only atom - )) +(defrecord Router [] + component/Lifecycle + (start + [component] + (if (:chsk-stop component) + component + (let [{:keys [send-fn ch-recv connected-uids ajax-post-fn ajax-get-or-ws-handshake-fn]} + (sente/make-channel-socket! {}) + + handler + (fn [{[id data :as event] :event + {{uid :uid :as session} :session :as ring-req} :ring-req + ?reply-fn :?reply-fn} + & [?recv]] + (let [reply (if (-> ?reply-fn meta :dummy-reply-fn?) + #(send-fn uid %) + ?reply-fn)] + (match + [id data] + [:omelette.page/search {:query query + :options options}] (->> (data/search query options) + (assoc data :results) + (vector id) + reply) + [:omelette.page/about _] (reply [id {:markdown data/about}]) + [:omelette.page/not-found _] (reply [id {}]) + :else (prn "Unmatched event: " event))))] + + (assoc component + :chsk-stop (sente/start-chsk-router-loop! handler ch-recv) + :send! send-fn + :recv ch-recv + :connected-uids connected-uids + :ring-routes + (let [render (render/renderer)] + (compojure/routes + (compojure/POST "/chsk" req (ajax-post-fn req)) + (compojure/GET "/chsk" req (ajax-get-or-ws-handshake-fn req)) + (compojure.route/resources "/") + (compojure/GET + "*" + {{uid :uid, :as session} :session, uri :uri, :as req} + (let [state (handler {:event (path->state uri) :ring-req req :?reply-fn identity})] + (assoc req + :status (if (-> state first name (= "not-found")) 404 200) + :session (assoc session :uid (or uid (java.util.UUID/randomUUID))) + :body (render (state->title state) (pr-str state))))))))))) + (stop + [component] + (when-let [chsk-stop (:chsk-stop component)] + (chsk-stop)) + (dissoc component :chsk-stop :send! :recv :connected-uids :ring-routes))) #+clj -(defn handler - [{[id data :as event] :event - {{uid :uid :as session} :session :as ring-req} :ring-req - ?reply-fn :?reply-fn :or {?reply-fn identity}} - & [?recv]] - (prn "Event: " event) - - (match - [id data] - - [:omelette.page/search {:query query - :options options}] (->> (data/search query options) - (assoc data :results) - (vector id) - ?reply-fn) - - [:omelette.page/about _] (?reply-fn [id {:markdown data/about}]) - - [:omelette.page/not-found _] (?reply-fn [id {}]) - - :else - (do (prn "Unmatched event: " event) - (when-not (:dummy-reply-fn? (meta ?reply-fn)) - (?reply-fn {:umatched-event-as-echoed-from-server event}))))) +(defn router [] + (map->Router {})) #+cljs -(defn handler [[id data :as ev] _] - (println "Event: " ev) - (match [id data] - ;; TODO Match your events here <...> - [:chsk/state {:first-open? true}] - (println "Channel socket successfully established!") - [:chsk/state new-state] (println "Chsk state change: %s" new-state) - [:chsk/recv payload] (println "Push event from server: %s" payload) - :else (println "Unmatched event: %s" ev))) - -(defn start! [] - (sente/start-chsk-router-loop! handler recv)) +(defn router [data owner opts] + (reify + + om/IDidMount + (did-mount + [_] + ; initialize history object and add it to state + (om/set-state! owner :history (doto (Html5History.) + (.setUseFragment false) + (.setPathPrefix "") + (.setEnabled true))) + ; listen for navigation events that originate from the browser + ; and update the app state based on the new token + (goog.events/listen (om/get-state owner :history) + EventType.NAVIGATE + #(when (.-isNavigation %) + (csp/put! (om/get-shared owner :nav-tokens) (.-token %)))) + ; update app state with new nav state + (->> (om/get-shared owner :nav-tokens) + (csp/map< path->state ) + (csp/reduce #(om/update! data [] %2 :nav) nil)) + ; initialize the channel socket and add it to state + (doseq [[k v] (rename-keys (sente/make-channel-socket! "/chsk" {}) + {:send-fn :send!, :state :chsk-state, :ch-recv :recv})] + (om/set-state! owner k v)) + ; start chsk router + (om/set-state! owner + :chsk-stop + (sente/start-chsk-router-loop! + (fn [event _] + (match + event + [:chsk/state {:first-open? true}] (println "Channel socket successfully established!") + [:chsk/state chsk-state] (println "Chsk state change: " chsk-state) + [:chsk/recv state] (om/update! data state) + :else (println "Unmatched event: " event))) + (om/get-state owner :recv))) + ; subscribe to transactions tagged :nav + (let [txs (csp/sub (om/get-shared owner :transactions-pub) :nav (csp/chan))] + (csp/go-loop + [timeout nil + {:keys [new-state old-state]} (csp/ new-state state->title (str " | Omelette"))) + ; update the token when the state changes. replace if it's a minor change; set if it's a page change + (let [history (om/get-state owner :history) + new-path (state->path new-state)] + (if (= (first old-state) + (first new-state)) + (.replaceToken history new-path) + (.setToken history new-path))) + (recur (js/setTimeout #((om/get-state owner :send!) new-state) 250) + (csp/ data first name)]) + (last data))))) diff --git a/src/omelette/serve.clj b/src/omelette/serve.clj new file mode 100644 index 0000000..07fbd99 --- /dev/null +++ b/src/omelette/serve.clj @@ -0,0 +1,32 @@ +(ns omelette.serve + (:require [com.stuartsierra.component :as component] + [compojure.core :as compojure] + [compojure.handler :as handler] + [org.httpkit.server :as http-kit] + [ring.middleware.anti-forgery :as ring-anti-forgery])) + +(defrecord Server [port] + component/Lifecycle + (start + [component] + (if (:server component) + component + (let [server + (-> component + :router + :ring-routes + (ring-anti-forgery/wrap-anti-forgery {:read-token #(-> % :params :csrf-token)}) + handler/site + (http-kit/run-server {:port (or port 0)})) + port + (-> server meta :local-port)] + (println "Web server running on port " port) + (assoc component :server server :port port)))) + (stop + [component] + (when-let [server (:server component)] + (server :timeout 250)) + (dissoc component :server :router))) + +(defn server [] + (map->Server {})) diff --git a/src/omelette/view.cljs b/src/omelette/view.cljs index d4aa478..88e6e99 100644 --- a/src/omelette/view.cljs +++ b/src/omelette/view.cljs @@ -1,177 +1,123 @@ (ns omelette.view (:require [ankha.core :as ankha] [cljs.core.async :as csp] - [cljs.core.match] [cljs.reader :as edn] [clojure.string :as str] - [goog.events :as events] + [goog.dom] [markdown.core :as md] [om.core :as om :include-macros true] [om.dom :as dom :include-macros true] [omelette.route :as route] - [sablono.core :as html :refer-macros [html]] - [taoensso.encore :as encore :refer [logf]] - [taoensso.sente :as sente]) - (:require-macros [cljs.core.match.macros :refer [match]] - [cljs.core.async.macros :as csp] - [omelette.view :refer [chsktitle %4))) - ; initialize state from document - (reset! app-state - (-> "omelette-state" - js/document.getElementById - .-textContent - edn/read-string))) - -(when (exists? js/window) - (let [history (doto (Html5History.) - (.setUseFragment false) - (.setPathPrefix "") - (.setEnabled true))] - ; update the token when the state changes - ; replace if it's a minor change - ; set of it's a major change - (add-watch app-state - :history-token - #(.setToken history (route/state->path %4)) -;; #(if (or (not= (first %3) -;; (first %4)) -;; (and (= (-> %4 first name) "search") -;; (-> %4 last :results))) -;; (.setToken history (route/state->path %4)) -;; (.replaceToken history (route/state->path %4))) - ) - - ; listen for navigation events that originate from the browser - ; and update the app state based on the new token - (events/listen history - EventType.NAVIGATE - #(when (.-isNavigation %) - (->> % .-token route/path->state (reset! app-state)))))) - -(defn- clean-state [[id data]] - [id (dissoc data :results :markdown)]) - - -(let [timeout (atom nil)] - (defn request-new-app-state! - ([state] - (route/send! (clean-state state) - 5000 - (fn [resp] - (if (sente/cb-success? resp) - (when (= (clean-state resp) (clean-state @app-state)) - (reset! app-state resp)) - (println "chsk callback failure: " {:sent (clean-state state) :response resp}))))) - ([state timeout-ms] - (js/clearTimeout @timeout) - (reset! timeout (js/setTimeout #(request-new-app-state! state) timeout-ms))))) - -(defn nav-link-to [href content] - [:a {:href href - :on-click (fn [e] - (.preventDefault e) - (reset! app-state (route/path->state href)))} - content]) - -(defn loading-view [& args] +(defn not-found-view [] (om/component (html - [:div - [:h2 "Loading..."] - (html/image "/assets/images/loading.gif")]))) + (html/image "/assets/images/404.gif")))) -(defn about-view [markdown] +(defn about-view [data] (om/component (html - [:div {:dangerouslySetInnerHTML {:__html (md/mdToHtml markdown)}}]))) - -(defn search-form-view [{:keys [query options] :as data} owner] + (if-let [markdown (:markdown data)] + (->> markdown + md/mdToHtml + (hash-map :__html) + (hash-map :dangerouslySetInnerHTML) + (vector :div)) + [:div + [:h3 "Loading..."] + (html/image "/assets/images/loading.gif")])))) + +(defn search-view [data owner] (reify - om/IRender - (render - [_] - (html [:form {:on-change (fn [_] (request-new-app-state! @app-state 300))} - [:input {:type "search" - :value query - :on-change #(om/update! data :query (-> % .-target .-value str/lower-case))}] - [:div {:on-click #(om/update! options ((if (-> % .-target .-checked) - conj - disj) - options - (-> % .-target .-name keyword)))} - (html/check-box "prefix" (-> options :prefix boolean)) - (html/label "prefix" "starts with") - [:br] - (html/check-box "infix" (-> options :infix boolean)) - (html/label "infix" "includes") - [:br] - (html/check-box "postfix" (-> options :postfix boolean)) - (html/label "postfix" "ends with") - [:br]]])))) - -(defn search-results-view [results] - (om/component - (html - (if (empty? results) - [:div "no results"] - (html/unordered-list results))))) - -(defn search-view [{:keys [results] :as data} owner] - (om/component - (html - [:div - (om/build search-form-view (dissoc data :results)) - (if results - (om/build search-results-view results) - (do (request-new-app-state! @app-state) - (om/build loading-view {}))) - ]))) + om/IInitState + (init-state [_] (dissoc (om/value data) :results)) + om/IRenderState + (render-state + [_ {:keys [query options]}] + (html + [:div + [:form {:on-change #(om/update! data [] (om/get-state owner) :nav) + :on-submit #(.preventDefault %)} + [:input {:type "search" + :value query + :on-change #(om/set-state! owner :query (-> % .-target .-value (str/replace #"\W|\d|_" "") str/lower-case))}] + [:br] + [:div {:on-change #(om/set-state! owner :options ((if (-> % .-target .-checked) + conj + disj) + options + (-> % .-target .-name keyword)))} + (html/check-box "prefix" (-> options :prefix boolean)) + (html/label "prefix" "starts with") + [:br] + (html/check-box "infix" (-> options :infix boolean)) + (html/label "infix" "includes") + [:br] + (html/check-box "postfix" (-> options :postfix boolean)) + (html/label "postfix" "ends with")]] + [:div + (if-let [results (:results data)] + (if (seq results) + (html/unordered-list results) + [:em "no results"]) + [:div + [:h3 "Loading..."] + (html/image "/assets/images/loading.gif")])]])))) (defn app-view [data owner] (om/component - (html - [:div - [:nav - (nav-link-to "/" "Search") - (nav-link-to "/about" "About")] - [:h1 (route/state->title data)] - (condp = (-> data first name) - "search" (om/build search-view (last data)) - "about" (if-let [m (-> data last :markdown)] - (om/build about-view m) - (do (request-new-app-state! @app-state) - (om/build loading-view {}))) - "not-found" (html/image "/assets/images/404.gif"))]))) + (letfn [(nav-link-to + [href content] + [:a {:href href + :on-click (fn [e] + (.preventDefault e) + (csp/put! (om/get-shared owner :nav-tokens) href))} + content])] + (html + [:div + [:div + [:nav + (nav-link-to "/" "Search") + (nav-link-to "/about" "About") + (nav-link-to "/not-found" "Not Found")] + [:h1 (route/state->title data)] + (om/build route/router + data + {:opts {:page-views {"about" about-view + "search" search-view + "not-found" not-found-view}}})] + [:div + (om/build ankha/inspector data)]])))) -(when (exists? js/window) - (set! (.-onload js/window) - (fn [] - (om/root app-view - app-state - {:target (js/document.getElementById "omelette-app")}) - (om/root ankha/inspector - app-state - {:target (js/document.getElementById "ankha")}) - (route/start!)))) +(def app-state (atom nil)) -; LT is chocking if we export this fn -(defn render-to-string [state-edn] +(defn render [] + (let [transactions (csp/chan) + transactions-pub (csp/pub transactions :tag)] + (om/root app-view + app-state + {:target (goog.dom/getElement "omelette-app") + :tx-listen #(csp/put! transactions %) + :shared {:nav-tokens (csp/chan) + :transactions transactions + :transactions-pub transactions-pub}}))) + +(defn ^:export render-to-string [state-edn] (->> state-edn edn/read-string (om/build app-view) dom/render-to-str)) + +(defn ^:export init [id] + (->> id + goog.dom/getElement + .-textContent + edn/read-string + (reset! app-state)) + (render)) diff --git a/src/user.clj b/src/user.clj new file mode 100644 index 0000000..72ca05c --- /dev/null +++ b/src/user.clj @@ -0,0 +1,28 @@ +(ns user + (:require [clojure.tools.namespace.repl :refer [refresh]] + [com.stuartsierra.component :as component] + [omelette.main :as main])) + +(defonce system + (atom main/system)) + +(defn reset [] + (reset! system main/system)) + +(defn start [] + (swap! system component/start)) + +(defn stop [] + (swap! system component/stop)) + +(defn restart [] + (stop) + (refresh :after 'user/start)) + +(comment + (reset) + (start) + (stop) + (restart) + (main/browse @system) + (prn @system))