I have a "Hello, World!" app in ClojureScript using Om (generated from the "Chestnut" lein template).
The goal is to have it set up such that:
document.location.hash
value reflects changes to the (:route app-state)
vector.(:route app-state)
vector reflects changes to the document.location.hash
value.(:route app-state)
changes.Note that I intend for the (:route app-state)
vector to be the only source of truth for the app about the app's current state. One mechanism of changing it is by the user modifying the url.
Where and how should I attach this behavior to Om?
Here's my "Hello, World!" app.
(ns demo.core
(:require [om.core :as om :include-macros true]
[om.dom :as dom :include-macros true]
[clojure.string :as string]))
(defonce app-state (atom {:text "Hello, World!"
:route ["some" "app" "route"]}))
(defn update-location-hash [app owner]
(reify
om/IRender
(render [_]
(set! js/window.location.hash
(string/join "/" (flatten ["#" (:route app)])))
(dom/div nil ""))))
(om.core/root
update-location-hash
app-state
{:target (. js/document (getElementById "app"))})
(defn main []
(om/root
(fn [app owner]
(reify
om/IRender
(render [_]
(dom/h1 nil (:text app)))))
app-state
{:target (. js/document (getElementById "app"))}))
This successfully writes the document.hash
on page load. Eventually this will be a single-page app that uses hash navigation to make view changes.
This feels dirty to me because of having to return a DOM element in the (render )
function of update-location-hash
that doesn't have any purpose other than to fulfill the requirements of the render
function.
Ok, I figured out what I needed. It seems pretty clean to me and works. For completeness, I would just need to add an on-page-load type listener that checks for the hash and a renderer that renders state changes to the hash. That should be pretty straight-forward (based on my prior code above).
(ns demo.core
(:require-macros [cljs.core.async.macros :refer [go]])
(:require [goog.events :as events]
[goog.events.EventType :as EventType]
[cljs.core.async :as async :refer [>! <! put! chan]]
[om.core :as om :include-macros true]
[om.dom :as dom :include-macros true]
[clojure.string :as string]))
;; Not sure what this does.
(enable-console-print!)
(defn listen
"An event listener factory. Given an element and an event type, return a
channel that can be polled for event outputs."
[el type]
(let [out (chan)] (events/listen el type #(put! out %)) out))
(defonce app-state
(atom {:text "Hello, World!"
:mouse [0 0]
:route ["some" "app" "route"]}))
(defn url-to-route
"Given a url, parse the hash value as an app route. In general, the
resultant route has these properties:
* route components are split on a solidus
* empty components are ignored (nill or all-whitespace)
A route like this:
['foo' 'bar' 'baz']
will be produced by all of the following urls (and others):
http://my-app.com/#/foo/bar/baz
http://my-app.com/#foo/bar/baz
http://my-app.com/#/foo// /bar//baz
http://my-app.com/#/ / / /foo/bar////baz"
[url]
;; Split the url at a hash followed by zero or more slashes and
;; whitespace. Then take anything after the hash and split it on one or
;; more slashes (ignoring whitespace).
(string/split (second (string/split url #"#[/\s]{0,}" 2)) #"[/\s]+"))
(defn layout
"The central application layout component. This registers global event
listeners and renders the application's root DOM nodes."
[app owner]
(reify
om/IWillMount
(will-mount [_]
;; Handle various events. When an event is triggered, format the
;; response.
(let [;; Listen for changes to the mouse position
mouse-chan (async/map
(fn [event] [(.-clientX event) (.-clientY event)])
[(listen js/window EventType/MOUSEMOVE)])
;; Listen for changes to the URL's hash
hash-chan (async/map
(fn [event] (url-to-route (-> event .-event_ .-newURL)))
[(listen js/window EventType/HASHCHANGE)])]
;; Watch the stream and update the application state whenever
;; anything changes.
(do
(go (while true (om/update! app :route (<! hash-chan))))
(go (while true (om/update! app :mouse (<! mouse-chan)))))))
om/IRender
(render [_]
(dom/div
nil
(dom/div nil (when-let [route (:route app)] (pr-str (:route app))))
(dom/div nil (when-let [pos (:mouse app)] (pr-str (:mouse app))))
(dom/h1 nil (:text app))))))
(defn main []
(om/root
layout
app-state
{:target (. js/document (getElementById "app"))}))