Skip to content

Commit 5b7990a

Browse files
committed
Implement HTML5 routing.
1 parent 2ca04e7 commit 5b7990a

5 files changed

Lines changed: 198 additions & 46 deletions

File tree

.dir-locals.el

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
((nil . ((fill-column . 70)
2+
(column-enforce-column . 70))))

deps.edn

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
com.taoensso/timbre {:mvn/version "4.10.0"}}
44
:aliases {:dev {:extra-paths ["dev"]
55
:jvm-opts ["-Dtrace"]
6-
:extra-deps {org.clojure/clojurescript {:mvn/version "1.10.597"}
7-
org.clojure/core.async {:mvn/version "1.0.567"}
8-
com.wsscode/async {:mvn/version "1.0.2"}
9-
thheller/shadow-cljs {:mvn/version "2.8.83"}
10-
fulcrologic/fulcro-inspect {:mvn/version "2.2.5"}
11-
binaryage/devtools {:mvn/version "0.9.10"}}}}}
6+
:extra-deps {org.clojure/clojurescript {:mvn/version "1.10.597"}
7+
com.fulcrologic/semantic-ui-wrapper {:mvn/version "1.0.0"}
8+
org.clojure/core.async {:mvn/version "1.0.567"}
9+
com.wsscode/async {:mvn/version "1.0.2"}
10+
clj-commons/pushy {:mvn/version "0.3.10"}
11+
edn-query-language/eql {:mvn/version "0.0.9"}
12+
thheller/shadow-cljs {:mvn/version "2.8.83"}
13+
binaryage/devtools {:mvn/version "0.9.10"}}}}}
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
(ns rocks.mygiftlist.application
2-
(:require [com.fulcrologic.fulcro.application :as app]))
2+
(:require [com.fulcrologic.fulcro.application :as app]
3+
[com.fulcrologic.fulcro.rendering.keyframe-render2 :as keyframe-render2]))
34

4-
(defonce SPA (app/fulcro-app {}))
5+
(defonce SPA (app/fulcro-app {:optimized-render! keyframe-render2/render!}))

src/rocks/mygiftlist/authentication.cljs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,5 @@
3232
(go-promise (<!p (.getTokenSilently @auth0-client))))
3333

3434
(defn get-user-info []
35-
(go-promise (<!p (.getUser @auth0-client))))
35+
(go-promise (js->clj (<!p (.getUser @auth0-client))
36+
:keywordize-keys true)))

src/rocks/mygiftlist/client.cljs

Lines changed: 183 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,167 @@
11
(ns rocks.mygiftlist.client
2-
(:require [rocks.mygiftlist.application :refer [SPA]]
3-
[rocks.mygiftlist.authentication :as auth]
4-
[com.fulcrologic.fulcro.application :as app]
5-
[com.fulcrologic.fulcro.components :as comp :refer [defsc]]
6-
[com.fulcrologic.fulcro.dom :as dom]
7-
[com.fulcrologic.fulcro.mutations :refer [defmutation]]
8-
[clojure.core.async :refer [go <!]]
9-
[clojure.string :as str]
10-
[taoensso.timbre :as log]))
11-
12-
(defsc CurrentUser [this {:user/keys [id email]
13-
:ui/keys [loading] :as user}]
14-
{:query [:user/id :user/email :ui/loading]
15-
:ident (fn [] [:component/id :current-user])
16-
:initial-state {:ui/loading true}}
17-
(cond
18-
loading (dom/button :.ui.loading.primary.button)
19-
(and id email) (dom/button :.ui.primary.button
20-
{:onClick #(auth/logout)}
21-
"Logout")
22-
:else (dom/button :.ui.primary.button
23-
{:onClick #(auth/login)}
24-
"Login/Signup")))
25-
26-
(def ui-current-user (comp/factory CurrentUser))
27-
28-
(defsc Root [this {:root/keys [current-user]}]
29-
{:query [{:root/current-user (comp/get-query CurrentUser)}]
30-
:initial-state {:root/current-user {}}}
2+
(:require
3+
[rocks.mygiftlist.application :refer [SPA]]
4+
[rocks.mygiftlist.authentication :as auth]
5+
[com.fulcrologic.fulcro.algorithms.normalized-state :refer [swap!->]]
6+
[com.fulcrologic.fulcro.application :as app]
7+
[com.fulcrologic.fulcro.components :as comp :refer [defsc]]
8+
[com.fulcrologic.fulcro.dom :as dom]
9+
[com.fulcrologic.fulcro.mutations :refer [defmutation]]
10+
[com.fulcrologic.fulcro.routing.dynamic-routing :as dr :refer [defrouter]]
11+
[clojure.core.async :as async :refer [go <!]]
12+
[clojure.string :as str]
13+
[edn-query-language.core :as eql]
14+
[pushy.core :as pushy]
15+
[taoensso.timbre :as log]))
16+
17+
(defn url->path
18+
"Given a url of the form \"/gift/123/edit?code=abcdef\", returns a
19+
path vector of the form [\"gift\" \"123\" \"edit\"]. Assumes the url
20+
starts with a forward slash. An empty url yields the path [\"home\"]
21+
instead of []."
22+
[url]
23+
(-> url (str/split "?") first (str/split "/") rest vec))
24+
25+
(defn path->url
26+
"Given a path vector of the form [\"gift\" \"123\" \"edit\"],
27+
returns a url of the form \"/gift/123/edit\"."
28+
[path]
29+
(str/join (interleave (repeat "/") path)))
30+
31+
(defn routable-path?
32+
"True if there exists a router target for the given path."
33+
[app path]
34+
(let [state-map (app/current-state app)
35+
root-class (app/root-class app)
36+
root-query (comp/get-query root-class state-map)
37+
ast (eql/query->ast root-query)]
38+
(some? (dr/ast-node-for-route ast path))))
39+
40+
(def default-route ["home"])
41+
42+
(defonce history (pushy/pushy
43+
(fn [path]
44+
(dr/change-route SPA path))
45+
(fn [url]
46+
(let [path (url->path url)]
47+
(if (routable-path? SPA path)
48+
path
49+
default-route)))))
50+
51+
(defn start! []
52+
(pushy/start! history))
53+
54+
(defn route-to! [path]
55+
(pushy/set-token! history (path->url path)))
56+
57+
(defmutation route-to
58+
"Mutation to go to a specific route"
59+
[{:keys [path]}]
60+
(action [_]
61+
(route-to! path)))
62+
63+
(defsc LoginLogoutItem [this {:ui/keys [authenticated]}]
64+
{:query [:ui/authenticated]
65+
:ident (fn [] [:component/id :login-logout])
66+
:initial-state {:ui/authenticated nil}}
67+
(when (some? authenticated)
68+
(if authenticated
69+
(dom/a :.item {:onClick #(auth/logout)} "Logout")
70+
(dom/a :.item {:onClick #(auth/login)} "Login/Signup"))))
71+
72+
(def ui-login-logout-item (comp/factory LoginLogoutItem))
73+
74+
(defmutation set-authenticated [{:keys [authenticated]}]
75+
(action [{:keys [state]}]
76+
(swap!-> state
77+
(assoc-in [:component/id :login-logout :ui/authenticated] authenticated)
78+
(assoc :root/loading false))))
79+
80+
(declare LoginForm Home About)
81+
82+
(defsc Navbar [this {:keys [login-logout]}]
83+
{:query [{:login-logout (comp/get-query LoginLogoutItem)}]
84+
:ident (fn [] [:component/id :navbar])
85+
:initial-state {:login-logout {}}}
86+
(let [logged-in (:ui/authenticated login-logout)]
87+
(dom/div :.ui.secondary.menu
88+
(dom/a :.item
89+
{:onClick #(comp/transact! this
90+
[(route-to {:path (dr/path-to (if logged-in
91+
Home
92+
LoginForm))})])}
93+
"Home")
94+
(dom/a :.item
95+
{:onClick #(comp/transact! this
96+
[(route-to {:path (dr/path-to About)})])}
97+
"About")
98+
(dom/div :.right.menu
99+
(ui-login-logout-item login-logout)))))
100+
101+
(def ui-navbar (comp/factory Navbar))
102+
103+
(defsc LoginForm [this _]
104+
{:query []
105+
:ident (fn [] [:component/id :login])
106+
:route-segment ["login"]
107+
:initial-state {}}
108+
(dom/div {}
109+
(dom/div "In order to view and create gift lists, you need to...")
110+
(dom/div (dom/button :.ui.primary.button
111+
{:onClick #(auth/login)}
112+
"Log in or sign up"))))
113+
114+
(defsc Home [this _]
115+
{:query []
116+
:ident (fn [] [:component/id :home])
117+
:initial-state {}
118+
:route-segment ["home"]}
31119
(dom/div {}
32-
(dom/div {} "Hello World")
33-
(ui-current-user current-user)))
120+
(dom/h3 {} "Home Screen")
121+
(dom/div {} "Welcome!")
122+
(dom/button :.ui.primary.button
123+
{:onClick #(auth/logout)}
124+
"Logout")))
125+
126+
(defsc About [this _]
127+
{:query []
128+
:ident (fn [] [:component/id :home])
129+
:initial-state {}
130+
:route-segment ["about"]}
131+
(dom/div {}
132+
(dom/h3 {} "About My Gift List")
133+
(dom/div {} "It's a really cool app!")))
134+
135+
(defn loading-spinner []
136+
(dom/div :.ui.active.inverted.dimmer
137+
(dom/div :.ui.loader)))
138+
139+
(defsc Loading [this _]
140+
{:query []
141+
:ident (fn [] [:component/id ::loading])
142+
:initial-state {}
143+
:route-segment ["loading"]}
144+
(loading-spinner))
145+
146+
(defrouter MainRouter [_ {:keys [current-state] :as props}]
147+
{:router-targets [Loading LoginForm Home About]}
148+
(loading-spinner))
149+
150+
(def ui-main-router (comp/factory MainRouter))
151+
152+
(defsc Root [this {:root/keys [router navbar loading]}]
153+
{:query [{:root/router (comp/get-query MainRouter)}
154+
{:root/navbar (comp/get-query Navbar)}
155+
:root/loading]
156+
:initial-state {:root/router {}
157+
:root/navbar {}
158+
:root/loading true}}
159+
(if loading
160+
(loading-spinner)
161+
(dom/div {}
162+
(ui-navbar navbar)
163+
(dom/div :.ui.container
164+
(ui-main-router router)))))
34165

35166
(defmutation set-current-user [user]
36167
(action [{:keys [state]}]
@@ -40,15 +171,30 @@
40171
(log/info "Hot code reload...")
41172
(app/mount! SPA Root "app"))
42173

174+
(defn- is-redirect? []
175+
(str/includes? (.. js/window -location -search) "code="))
176+
177+
(defn- clear-query-params! []
178+
(.replaceState js/window.history #js {} js/document.title js/window.location.pathname))
179+
43180
(defn ^:export init []
44181
(log/info "Application starting...")
45182
(app/mount! SPA Root "app")
183+
(dr/initialize! SPA)
184+
(pushy/start! history)
46185
(go
47186
(<! (auth/create-auth0-client!))
48-
(when (str/includes? (.. js/window -location -search) "code=")
187+
(when (is-redirect?)
49188
(<! (auth/handle-redirect-callback))
50-
(.replaceState js/window.history #js {} js/document.title js/window.location.pathname))
51-
(if-let [authenticated (<! (auth/is-authenticated?))]
52-
(let [{:strs [sub email]} (js->clj (<! (auth/get-user-info)))]
53-
(comp/transact! SPA [(set-current-user {:user/id sub :user/email email})]))
54-
(comp/transact! SPA [(set-current-user {})]))))
189+
(clear-query-params!))
190+
(let [authenticated (<! (auth/is-authenticated?))]
191+
(comp/transact! SPA [(set-authenticated
192+
{:authenticated authenticated})])
193+
(if authenticated
194+
(do (comp/transact! SPA
195+
[(route-to {:path (url->path js/window.location.pathname)})])
196+
(let [{:keys [sub email]} (<! (auth/get-user-info))]
197+
(comp/transact! SPA [(set-current-user
198+
#:user{:id sub :email email})])))
199+
(comp/transact! SPA
200+
[(route-to {:path (dr/path-to LoginForm)})])))))

0 commit comments

Comments
 (0)