mirror of
https://github.com/metosin/reitit.git
synced 2025-12-16 16:01:11 +00:00
Create example
This commit is contained in:
parent
468a0947d2
commit
417f35a318
12 changed files with 322 additions and 79 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -11,3 +11,4 @@ pom.xml.asc
|
|||
/gh-pages
|
||||
/node_modules
|
||||
/_book
|
||||
figwheel_server.log
|
||||
|
|
|
|||
1
examples/frontend/checkouts/reitit-core
Symbolic link
1
examples/frontend/checkouts/reitit-core
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../../modules/reitit-core
|
||||
1
examples/frontend/checkouts/reitit-frontend
Symbolic link
1
examples/frontend/checkouts/reitit-frontend
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../../modules/reitit-frontend
|
||||
1
examples/frontend/checkouts/reitit-schema
Symbolic link
1
examples/frontend/checkouts/reitit-schema
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../../modules/reitit-schema
|
||||
51
examples/frontend/project.clj
Normal file
51
examples/frontend/project.clj
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
(defproject frontend "0.1.0-SNAPSHOT"
|
||||
:description "FIXME: write description"
|
||||
:url "http://example.com/FIXME"
|
||||
:license {:name "Eclipse Public License"
|
||||
:url "http://www.eclipse.org/legal/epl-v10.html"}
|
||||
|
||||
:dependencies [[org.clojure/clojure "1.9.0"]
|
||||
[ring-server "0.5.0"]
|
||||
[reagent "0.8.1"]
|
||||
[ring "1.6.3"]
|
||||
[compojure "1.6.1"]
|
||||
[hiccup "1.0.5"]
|
||||
[org.clojure/clojurescript "1.10.238" :scope "provided"]
|
||||
[metosin/reitit "0.1.3"]
|
||||
[metosin/reitit-schema "0.1.3"]
|
||||
[metosin/reitit-frontend "0.1.3"]]
|
||||
|
||||
:plugins [[lein-cljsbuild "1.1.7"]]
|
||||
|
||||
:source-paths []
|
||||
:resource-paths ["resources" "target/cljsbuild"]
|
||||
|
||||
:cljsbuild
|
||||
{:builds
|
||||
[{:id "app"
|
||||
:figwheel true
|
||||
:source-paths ["src"]
|
||||
:watch-paths ["src" "checkouts/reitit-frontend/src"]
|
||||
:compiler {:main "frontend.core"
|
||||
:asset-path "/js/out"
|
||||
:output-to "target/cljsbuild/public/js/app.js"
|
||||
:output-dir "target/cljsbuild/public/js/out"
|
||||
:source-map true
|
||||
:optimizations :none
|
||||
:pretty-print true
|
||||
:preloads [devtools.preload]}}
|
||||
{:id "min"
|
||||
:source-paths ["src"]
|
||||
:compiler {:output-to "target/cljsbuild/public/js/app.js"
|
||||
:output-dir "target/cljsbuild/public/js"
|
||||
:source-map "target/cljsbuild/public/js/app.js.map"
|
||||
:optimizations :advanced
|
||||
:pretty-print false}}]}
|
||||
|
||||
:figwheel
|
||||
{:http-server-root "public"
|
||||
:server-port 3449
|
||||
:nrepl-port 7002}
|
||||
|
||||
:profiles {:dev {:dependencies [[binaryage/devtools "0.9.10"]]
|
||||
:plugins [[lein-figwheel "0.5.16"]]}})
|
||||
10
examples/frontend/resources/public/index.html
Normal file
10
examples/frontend/resources/public/index.html
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Reitit frontend example</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script src="js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
50
examples/frontend/src/frontend/core.cljs
Normal file
50
examples/frontend/src/frontend/core.cljs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
(ns frontend.core
|
||||
(:require [reagent.core :as r]
|
||||
[reitit.core :as rc]
|
||||
[reitit.frontend :as rf]
|
||||
[reitit.frontend.history :as rfh]
|
||||
[reitit.coercion.schema :as rs]))
|
||||
|
||||
(def router (atom nil))
|
||||
|
||||
(defn home-page []
|
||||
[:div
|
||||
[:h2 "Welcome to frontend"]
|
||||
[:div [:a {:href (rfh/href @router ::about)} "go to about page"]]])
|
||||
|
||||
(defn about-page []
|
||||
[:div
|
||||
[:h2 "About frontend"]
|
||||
[:a {:href "http://google.com"} "external link"]
|
||||
[:div [:a {:href (rfh/href @router ::frontpage)} "go to the home page"]]])
|
||||
|
||||
(defonce match (r/atom nil))
|
||||
|
||||
(defn current-page []
|
||||
[:div
|
||||
(if @match
|
||||
[:div [(:view (:data @match))]])
|
||||
(pr-str @match)])
|
||||
|
||||
(def routes
|
||||
(rc/router
|
||||
[""
|
||||
[""
|
||||
{:name ::frontpage-root
|
||||
:view home-page}]
|
||||
["/"
|
||||
[""
|
||||
{:name ::frontpage
|
||||
:view home-page}]
|
||||
["about"
|
||||
{:name ::about
|
||||
:view about-page}]]]))
|
||||
|
||||
(defn init! []
|
||||
(reset! router (rfh/start! routes
|
||||
(fn [m] (reset! match m))
|
||||
{:use-fragment true
|
||||
:path-prefix "/"}))
|
||||
(r/render [current-page] (.getElementById js/document "app")))
|
||||
|
||||
(init!)
|
||||
|
|
@ -1,19 +1,14 @@
|
|||
(ns reitit.frontend
|
||||
"Utilities to implement frontend routing using Reitit.
|
||||
|
||||
Controller is way to declare as data the side-effects and optionally
|
||||
other data related to the route."
|
||||
""
|
||||
(:require [reitit.core :as reitit]
|
||||
[clojure.string :as str]
|
||||
goog.Uri
|
||||
[reitit.coercion :as coercion]))
|
||||
|
||||
;;
|
||||
;; Utilities
|
||||
;;
|
||||
[reitit.coercion :as coercion]
|
||||
[goog.events :as e]
|
||||
[goog.dom :as dom])
|
||||
(:import goog.Uri))
|
||||
|
||||
(defn query-params
|
||||
"Parse query-params from URL into a map."
|
||||
"Given goog.Uri, read query parameters into Clojure map."
|
||||
[^goog.Uri uri]
|
||||
(let [q (.getQueryData uri)]
|
||||
(->> q
|
||||
|
|
@ -21,70 +16,43 @@
|
|||
(map (juxt keyword #(.get q %)))
|
||||
(into {}))))
|
||||
|
||||
(defn get-hash
|
||||
"Given browser hash starting with #, remove the # and
|
||||
end slashes."
|
||||
[]
|
||||
(-> js/location.hash
|
||||
(subs 1)
|
||||
(str/replace #"/$" "")))
|
||||
(defn query-string
|
||||
"Given map, creates "
|
||||
[m]
|
||||
(str/join "&" (map (fn [[k v]]
|
||||
(str (js/encodeURIComponent (name k))
|
||||
"="
|
||||
;; FIXME: create protocol to handle how types are converted to string
|
||||
;; FIXME: array to multiple params
|
||||
(if (coll? v)
|
||||
(str/join "," (map #(js/encodeURIComponent %) v))
|
||||
(js/encodeURIComponent v))))
|
||||
m)))
|
||||
|
||||
;;
|
||||
;; Controller implementation
|
||||
;;
|
||||
(defn match-by-path
|
||||
"Given routing tree and current path, return match with possibly
|
||||
coerced parameters. Return nil if no match found."
|
||||
[router path]
|
||||
(let [uri (.parse Uri path)]
|
||||
(if-let [match (reitit/match-by-path router (.getPath uri))]
|
||||
(let [q (query-params uri)
|
||||
;; Return uncoerced values if coercion is not enabled - so
|
||||
;; that tha parameters are always accessible from same property.
|
||||
;; FIXME: coerce! can't be used as it doesn't take query-params
|
||||
parameters (if (:result match)
|
||||
(coercion/coerce-request (:result match) {:query-params q
|
||||
:path-params (:path-params match)})
|
||||
{:query q
|
||||
:path (:param match)})]
|
||||
(assoc match :parameters parameters)))))
|
||||
|
||||
(defn get-params
|
||||
"Get controller parameters given match. If controller provides :params
|
||||
function that will be called with the match. Default is nil."
|
||||
[controller match]
|
||||
(if-let [f (:params controller)]
|
||||
(f match)))
|
||||
|
||||
(defn apply-controller
|
||||
"Run side-effects (:start or :stop) for controller.
|
||||
The side-effect function is called with controller params."
|
||||
[controller method]
|
||||
(when-let [f (get controller method)]
|
||||
(f (::params controller))))
|
||||
|
||||
(defn- pad-same-length [a b]
|
||||
(concat a (take (- (count b) (count a)) (repeat nil))))
|
||||
|
||||
(defn apply-controllers
|
||||
"Applies changes between current controllers and
|
||||
those previously enabled. Resets controllers whose
|
||||
parameters have changed."
|
||||
[old-controllers new-match]
|
||||
(let [new-controllers (map (fn [controller]
|
||||
(assoc controller ::params (get-params controller new-match)))
|
||||
(:controllers (:data new-match)))
|
||||
changed-controllers (->> (map (fn [old new]
|
||||
;; different controllers, or params changed
|
||||
(if (not= old new)
|
||||
{:old old, :new new}))
|
||||
(pad-same-length old-controllers new-controllers)
|
||||
(pad-same-length new-controllers old-controllers))
|
||||
(keep identity)
|
||||
vec)]
|
||||
(doseq [controller (map :old changed-controllers)]
|
||||
(apply-controller controller :stop))
|
||||
(doseq [controller (map :new changed-controllers)]
|
||||
(apply-controller controller :start))
|
||||
new-controllers))
|
||||
|
||||
(defn hash-change [router hash]
|
||||
(let [uri (goog.Uri/parse hash)
|
||||
match (or (reitit/match-by-path router (.getPath uri))
|
||||
{:data {:name :not-found}})
|
||||
q (query-params uri)
|
||||
;; Coerce if coercion enabled
|
||||
c (if (:result match)
|
||||
(coercion/coerce-request (:result match) {:query-params q
|
||||
:path-params (:params match)})
|
||||
{:query q
|
||||
:path (:param match)})
|
||||
;; Replace original params with coerced params
|
||||
match (-> match
|
||||
(assoc :query (:query c))
|
||||
(assoc :params (:path c)))]
|
||||
match))
|
||||
(defn match-by-name
|
||||
[router name params]
|
||||
;; FIXME: move router not initialized to re-frame integration?
|
||||
(if router
|
||||
(or (reitit/match-by-name router name params)
|
||||
;; FIXME: return nil?
|
||||
(do
|
||||
(js/console.error "Can't create URL for route " (pr-str name) (pr-str params))
|
||||
nil))
|
||||
::not-initialized))
|
||||
|
|
|
|||
114
modules/reitit-frontend/src/reitit/frontend/history.cljs
Normal file
114
modules/reitit-frontend/src/reitit/frontend/history.cljs
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
(ns reitit.frontend.history
|
||||
""
|
||||
(:require [reitit.core :as reitit]
|
||||
[clojure.string :as string]
|
||||
[goog.events :as e]
|
||||
[goog.dom :as dom]
|
||||
[reitit.frontend :as rf])
|
||||
(:import goog.history.Html5History
|
||||
goog.Uri))
|
||||
|
||||
;; Token is for Closure HtmlHistory
|
||||
;; Path is for reitit
|
||||
|
||||
(defn- token->path [history token]
|
||||
(if (.-useFragment_ history)
|
||||
token
|
||||
(str (.getPathPrefix history) token)))
|
||||
|
||||
(defn- path->token [history path]
|
||||
(subs path (if (.-useFragment_ history)
|
||||
1
|
||||
(count (.getPathPrefix history)))))
|
||||
|
||||
(defn- token->href [history token]
|
||||
(str (if (.-useFragment_ history)
|
||||
(str "#"))
|
||||
(.getPathPrefix history)
|
||||
token))
|
||||
|
||||
(def ^:private current-domain (.getDomain (.parse Uri js/location)))
|
||||
|
||||
(defn ignore-anchor-click
|
||||
"Ignore click events from a elements, if the href points to URL that is part
|
||||
of the routing tree."
|
||||
[router history e]
|
||||
;; Returns the next matching anchestor of event target
|
||||
(when-let [el (.closest (.-target e) "a")]
|
||||
(let [uri (.parse Uri (.-href el))]
|
||||
(when (and (or (and (not (.hasScheme uri)) (not (.hasDomain uri)))
|
||||
(= current-domain (.getDomain uri)))
|
||||
(not (.-altKey e))
|
||||
(not (.-ctrlKey e))
|
||||
(not (.-metaKey e))
|
||||
(not (.-shiftKey e))
|
||||
(not (contains? #{"_blank" "self"} (.getAttribute el "target")))
|
||||
;; Left button
|
||||
(= 0 (.-button e))
|
||||
(reitit/match-by-path router (.getPath uri)))
|
||||
(.preventDefault e)
|
||||
(.replaceToken history (path->token history (.getPath uri)))))))
|
||||
|
||||
(defn start!
|
||||
"Parameters:
|
||||
- router The reitit routing tree.
|
||||
- on-navigate Function to be called when route changes.
|
||||
|
||||
Options:
|
||||
- :use-fragment (default true) If true, onhashchange and location hash are used to store the token.
|
||||
- :path-prefix (default \"/\") If :use-fragment is false, this is prepended to all tokens, and is
|
||||
removed from start of the token before matching the route."
|
||||
[router
|
||||
on-navigate
|
||||
{:keys [path-prefix use-fragment]
|
||||
:or {path-prefix "/"
|
||||
use-fragment true}}]
|
||||
(let [history
|
||||
(doto (Html5History.)
|
||||
(.setEnabled true)
|
||||
(.setPathPrefix path-prefix)
|
||||
(.setUseFragment use-fragment))
|
||||
|
||||
event-key
|
||||
(e/listen history goog.history.EventType.NAVIGATE
|
||||
(fn [e]
|
||||
(on-navigate (rf/match-by-path router (token->path history (.getToken history))))))
|
||||
|
||||
click-listen-key
|
||||
(if-not use-fragment
|
||||
(e/listen js/document e/EventType.CLICK
|
||||
(partial ignore-anchor-click router history)))]
|
||||
|
||||
;; Trigger navigate event for current route
|
||||
(on-navigate (rf/match-by-path router (token->path history (.getToken history))))
|
||||
|
||||
{:router router
|
||||
:history history
|
||||
:close-fn (fn []
|
||||
(e/unlistenByKey event-key)
|
||||
(e/unlistenByKey click-listen-key)
|
||||
(.setEnabled history false))}))
|
||||
|
||||
(defn stop! [{:keys [close-fn]}]
|
||||
(if close-fn
|
||||
(close-fn)))
|
||||
|
||||
(defn- match->token [history match k params]
|
||||
;; FIXME: query string
|
||||
(if-let [path (:path match)]
|
||||
(path->token history path)))
|
||||
|
||||
(defn href
|
||||
([state k]
|
||||
(href state k nil))
|
||||
([state k params]
|
||||
(href state k params nil))
|
||||
([{:keys [router history]} k params query]
|
||||
(let [match (rf/match-by-name router k params)
|
||||
token (match->token history match k params)]
|
||||
(token->href history token))))
|
||||
|
||||
(defn replace-token [{:keys [router history]} k params]
|
||||
(let [match (rf/match-by-name router k params)
|
||||
token (match->token history match k params)]
|
||||
(.replaceToken history token)))
|
||||
|
|
@ -62,7 +62,10 @@
|
|||
[criterium "0.4.4"]
|
||||
[org.clojure/test.check "0.9.0"]
|
||||
[org.clojure/tools.namespace "0.2.11"]
|
||||
[com.gfredericks/test.chuck "0.2.9"]]}
|
||||
[com.gfredericks/test.chuck "0.2.9"]
|
||||
|
||||
;; https://github.com/bensu/doo/issues/180
|
||||
[fipp "0.6.12"]]}
|
||||
:perf {:jvm-opts ^:replace ["-server"
|
||||
"-Xmx4096m"
|
||||
"-Dclojure.compiler.direct-linking=true"]
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@
|
|||
reitit.impl-test
|
||||
reitit.middleware-test
|
||||
reitit.ring-test
|
||||
#_reitit.spec-test))
|
||||
#_reitit.spec-test
|
||||
reitit.frontend.core-test))
|
||||
|
||||
(enable-console-print!)
|
||||
|
||||
|
|
@ -14,4 +15,5 @@
|
|||
'reitit.impl-test
|
||||
'reitit.middleware-test
|
||||
'reitit.ring-test
|
||||
#_'reitit.spec-test)
|
||||
#_'reitit.spec-test
|
||||
'reitit.frontend.core-test)
|
||||
|
|
|
|||
41
test/cljs/reitit/frontend/core_test.cljs
Normal file
41
test/cljs/reitit/frontend/core_test.cljs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
(ns reitit.frontend.core-test
|
||||
(:require [clojure.test :refer [deftest testing is are]]
|
||||
[reitit.core :as r]
|
||||
[reitit.frontend :as rf]
|
||||
[reitit.coercion :as coercion]
|
||||
[schema.core :as s]
|
||||
[reitit.coercion.schema :as schema]))
|
||||
|
||||
(deftest match-by-path-test
|
||||
(testing "simple"
|
||||
(let [router (r/router ["/"
|
||||
["" ::frontpage]
|
||||
["foo" ::foo]
|
||||
["bar" ::bar]])]
|
||||
(is (= {:template "/"
|
||||
:data {:name ::frontpage}
|
||||
:path "/"}
|
||||
(rf/match-by-path router "/")))
|
||||
(is (= {:template "/foo"
|
||||
:data {:name ::foo}
|
||||
:path "/foo"}
|
||||
(rf/match-by-path router "/foo")))))
|
||||
|
||||
(testing "schema coercion"
|
||||
(let [router (r/router ["/"
|
||||
[":id"
|
||||
{:name ::foo
|
||||
:parameters {:path {:id s/Int}
|
||||
:query {(s/optional-key :mode) s/Keyword}}}]])]
|
||||
#_#_
|
||||
(is (= {:template "/5"
|
||||
:data {:name ::foo}
|
||||
:path "/5"
|
||||
:parameters {:path {:id 5}}}
|
||||
(rf/match-by-path router "/5")))
|
||||
(is (= {:template "/5?mode=foo"
|
||||
:data {:name ::foo}
|
||||
:path "/5?mode=foo"
|
||||
:parameters {:path {:id 5}
|
||||
:query {:mode :foo}}}
|
||||
(rf/match-by-path router "/5?mode=foo"))))))
|
||||
Loading…
Reference in a new issue