mirror of
https://github.com/metosin/reitit.git
synced 2025-12-17 00:11: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
|
/gh-pages
|
||||||
/node_modules
|
/node_modules
|
||||||
/_book
|
/_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
|
(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]
|
(:require [reitit.core :as reitit]
|
||||||
[clojure.string :as str]
|
[clojure.string :as str]
|
||||||
goog.Uri
|
[reitit.coercion :as coercion]
|
||||||
[reitit.coercion :as coercion]))
|
[goog.events :as e]
|
||||||
|
[goog.dom :as dom])
|
||||||
;;
|
(:import goog.Uri))
|
||||||
;; Utilities
|
|
||||||
;;
|
|
||||||
|
|
||||||
(defn query-params
|
(defn query-params
|
||||||
"Parse query-params from URL into a map."
|
"Given goog.Uri, read query parameters into Clojure map."
|
||||||
[^goog.Uri uri]
|
[^goog.Uri uri]
|
||||||
(let [q (.getQueryData uri)]
|
(let [q (.getQueryData uri)]
|
||||||
(->> q
|
(->> q
|
||||||
|
|
@ -21,70 +16,43 @@
|
||||||
(map (juxt keyword #(.get q %)))
|
(map (juxt keyword #(.get q %)))
|
||||||
(into {}))))
|
(into {}))))
|
||||||
|
|
||||||
(defn get-hash
|
(defn query-string
|
||||||
"Given browser hash starting with #, remove the # and
|
"Given map, creates "
|
||||||
end slashes."
|
[m]
|
||||||
[]
|
(str/join "&" (map (fn [[k v]]
|
||||||
(-> js/location.hash
|
(str (js/encodeURIComponent (name k))
|
||||||
(subs 1)
|
"="
|
||||||
(str/replace #"/$" "")))
|
;; 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)))
|
||||||
|
|
||||||
;;
|
(defn match-by-path
|
||||||
;; Controller implementation
|
"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
|
(defn match-by-name
|
||||||
"Get controller parameters given match. If controller provides :params
|
[router name params]
|
||||||
function that will be called with the match. Default is nil."
|
;; FIXME: move router not initialized to re-frame integration?
|
||||||
[controller match]
|
(if router
|
||||||
(if-let [f (:params controller)]
|
(or (reitit/match-by-name router name params)
|
||||||
(f match)))
|
;; FIXME: return nil?
|
||||||
|
(do
|
||||||
(defn apply-controller
|
(js/console.error "Can't create URL for route " (pr-str name) (pr-str params))
|
||||||
"Run side-effects (:start or :stop) for controller.
|
nil))
|
||||||
The side-effect function is called with controller params."
|
::not-initialized))
|
||||||
[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))
|
|
||||||
|
|
|
||||||
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"]
|
[criterium "0.4.4"]
|
||||||
[org.clojure/test.check "0.9.0"]
|
[org.clojure/test.check "0.9.0"]
|
||||||
[org.clojure/tools.namespace "0.2.11"]
|
[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"
|
:perf {:jvm-opts ^:replace ["-server"
|
||||||
"-Xmx4096m"
|
"-Xmx4096m"
|
||||||
"-Dclojure.compiler.direct-linking=true"]
|
"-Dclojure.compiler.direct-linking=true"]
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,8 @@
|
||||||
reitit.impl-test
|
reitit.impl-test
|
||||||
reitit.middleware-test
|
reitit.middleware-test
|
||||||
reitit.ring-test
|
reitit.ring-test
|
||||||
#_reitit.spec-test))
|
#_reitit.spec-test
|
||||||
|
reitit.frontend.core-test))
|
||||||
|
|
||||||
(enable-console-print!)
|
(enable-console-print!)
|
||||||
|
|
||||||
|
|
@ -14,4 +15,5 @@
|
||||||
'reitit.impl-test
|
'reitit.impl-test
|
||||||
'reitit.middleware-test
|
'reitit.middleware-test
|
||||||
'reitit.ring-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