Skip to content

Commit

Permalink
comment omellete.route
Browse files Browse the repository at this point in the history
  • Loading branch information
Dom Kiva-Meyer committed Jun 11, 2014
1 parent 6ea23a0 commit 7021014
Showing 1 changed file with 100 additions and 55 deletions.
155 changes: 100 additions & 55 deletions src/omelette/route.cljx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,11 @@
:options (decode-search-options options)}]
[:omelette.page/not-found {}]))

(defn path->state [path]
(defn path->state
"Converts a path to an app state.
(path->state \"/search/prefix/omelette\")
=> [:omelette.page/search {:query \"omelette\" :options #{:prefix}}]"
[path]
(match
(filterv (complement str/blank?) (-> path str/lower-case (str/split #"/")))
[] (search->state "omelette" "prefix-infix-postfix")
Expand All @@ -56,7 +60,11 @@
["about"] [:omelette.page/about {}]
:else [:omelette.page/not-found {}]))

(defn state->path [[k data]]
(defn state->path
"Converts an app state to a path.
(state->path [:omelette.page/search {:query \"omelette\" :options #{:prefix}}])
=> \"/search/prefix/omelette\""
[[k data]]
(let [page (name k)]
(if (= page "search")
(str "/search/"
Expand All @@ -65,7 +73,11 @@
(:query data))
(str "/" page))))

(defn state->title [[k data]]
(defn state->title
"Converts an app state to a title.
(state->title [:omelette.page/search {:query \"omelette\" :options #{:prefix}}])
=> \"words that begin with \"omelette\"\""
[[k data]]
(let [page (name k)]
(if (= page "search")
(let [{:keys [query options]} data
Expand All @@ -83,91 +95,121 @@
(map str/capitalize)
(str/join " ")))))




#+clj
(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]}
(let [; Create a Sente channel socket (chsk)
{:keys [send-fn ch-recv connected-uids ajax-post-fn ajax-get-or-ws-handshake-fn]}
(sente/make-channel-socket! {})

; Function to handle events.
; Events can be sent from clients or server and will respond with a modified app state either way
handler
(fn [{[id data :as event] :event
(fn [{[page 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 %)
(let [reply
; Sente passes a dummy reply fn if once is not provided on the client-side
; This usage of Sente is probably unusual since it calls the handler fn directly below,
; so use a different reply fn unless one is passed in directly or from the client.
(if (-> ?reply-fn meta :dummy-reply-fn?)
#(send-fn uid %) ; Reply by sending the new state back to the client.
?reply-fn)]
(match
[id data]
event
[:omelette.page/search {:query query
:options options}] (->> (data/search query options)
(assoc data :results)
(vector id)
(vector page)
reply)
[:omelette.page/about _] (reply [id {:markdown data/about}])
[:omelette.page/not-found _] (reply [id {}])
[:omelette.page/about _] (reply [page {:markdown (data/about)}])
[:omelette.page/not-found _] (reply [page {}])
:else (prn "Unmatched event: " event))))]

; Return a started router component that can be used by the server component.
(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)]
:chsk-stop (sente/start-chsk-router-loop! handler ch-recv) ; Function to stop the router loop.
:send! send-fn ; Function to send messages to connected clients.
:recv ch-recv ; Channel that receives events send from clients.
:connected-uids connected-uids ; Atom of connected client UIDs
:ring-routes ; Ring routes to be used by the server component.
(let [render (render/renderer)] ; Create a new render function.
(compojure/routes
(compojure/POST "/chsk" req (ajax-post-fn req))
(compojure/POST "/chsk" req (ajax-post-fn req)) ; /chsk routes for Sente.
(compojure/GET "/chsk" req (ajax-get-or-ws-handshake-fn req))
(compojure.route/resources "/")
(compojure.route/resources "/") ; Serve static resources.
(compojure/GET
"*"
"*" ; Wildcard route that will render fully-formed HTML.
{{uid :uid, :as session} :session, uri :uri, :as req}
; Call the handler function directly and get the result using `identity` as the reply-fn.
(let [state (handler {:event (path->state uri) :ring-req req :?reply-fn identity})]
(assoc req
:status (if (-> state first name (= "not-found")) 404 200)
; Pass the title and the state to the render fn and assoc the returned HTML.
:body (render (state->title state) (pr-str state))
; Clients must have a UID in order to send messages to them.
:session (assoc session :uid (or uid (java.util.UUID/randomUUID)))
:body (render (state->title state) (pr-str state)))))))))))
; Use the state to determine the status.
:status (if (-> state first name (= "not-found")) 404 200))))))))))
(stop
[component]
(when-let [chsk-stop (:chsk-stop component)]
(chsk-stop))
(chsk-stop)) ; Stop the chsk loop.
(dissoc component :chsk-stop :send! :recv :connected-uids :ring-routes)))

#+clj
(defn router []
(defn router
"Creates a router component.
Key :ring-routes should be used by a parent component."
[]
(map->Router {}))

#+cljs
(defn router [data owner opts]
(defn router
"Creates a router component.
:page-views key in opts should be a map of page name to page views:
{:page-views {\"about\" about-view
\"not-found\" not-found-view}}
Shared :nav-tokens should be a channel onto which other components should put relative paths when links are clicked.
Shared :transactions-pub should be publication of transactions with :tag as the topic-fn."
[data owner opts]
(reify

om/IRender
(render
[_]
(om/build ; Build the page view and pass in the page data.
(get-in opts [:page-views (-> data first name)])
(last data)))
; Initialize things that are incompatible with Nashorn (anything related to `window` or `document`)
; or unnecessary (core.async loops).
om/IDidMount
(did-mount
[_]
; initialize history object and add it to state
; Initialize history object and add it to local 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
; Listen for navigation events that originate from the browser
; and update the app state based on the new path.
(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
; Update app state with state derived from navigation tokens.
(->> (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
; Initialize channel socket and add it to local 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
; Start channel socket router loop and add stop function to local state.
(om/set-state! owner
:chsk-stop
(sente/start-chsk-router-loop!
Expand All @@ -176,38 +218,41 @@
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)))
; Events sent from the server have an ID of `:chsk/recv`.
; Update app state with the new state.
; This is a potential bug since events are not guaranteed to be sequential.
[:chsk/recv state] (when (= (first state)
(first @data))
(om/update! data state))

:else (prn "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))]

(let [txs (csp/sub (om/get-shared owner :transactions-pub) :nav (csp/chan))
send! (om/get-state owner :send!)]
(csp/go-loop
[timeout nil
{:keys [new-state old-state]} (csp/<! txs)]
; Cancel timeout set below.
(js/clearTimeout timeout)
; change document title to reflect app state
; Change document title to reflect new app state.
(set! js/document.title (-> 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
; Update the token when the state changes.
(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)
(if-not (= (first old-state)
(first new-state))
(.setToken history new-path) ; Set when page changes;
(.replaceToken history new-path))) ; replace otherwise.
(recur (js/setTimeout #(send! new-state) 250)
(csp/<! txs)))))

; Clean up so that router can be safely rendered multiple times.
om/IWillUnmount
(will-unmount
[_]
(let [history (om/get-state owner :history)]
(goog.events/removeAll history)
(.setEnabled history false))
((om/get-state owner :chsk-stop)))
(goog.events/removeAll history) ; Remove goog.events listeners from history object.
(.setEnabled history false)) ; Disable history object.
((om/get-state owner :chsk-stop))) ; Stop channel socket loop.

om/IRender
(render
[_]
(om/build
(get-in opts [:page-views (-> data first name)])
(last data)))))
))

0 comments on commit 7021014

Please sign in to comment.