diff --git a/.gitignore b/.gitignore
index 73f41df4..7b183f21 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,4 @@
-/target
+target/
/classes
/checkouts
pom.xml
@@ -11,3 +11,4 @@ pom.xml.asc
/gh-pages
/node_modules
/_book
+figwheel_server.log
diff --git a/README.md b/README.md
index d5c5a4bc..aa540f0d 100644
--- a/README.md
+++ b/README.md
@@ -36,18 +36,18 @@ Bubblin' under:
All bundled:
```clj
-[metosin/reitit "0.1.3"]
+[metosin/reitit "0.1.4-SNAPSHOT"]
```
Optionally, the parts can be required separately:
```clj
-[metosin/reitit-core "0.1.3"]
-[metosin/reitit-ring "0.1.3"]
-[metosin/reitit-spec "0.1.3"]
-[metosin/reitit-schema "0.1.3"]
-[metosin/reitit-swagger "0.1.3"]
-[metosin/reitit-swagger-ui "0.1.3"]
+[metosin/reitit-core "0.1.4-SNAPSHOT"]
+[metosin/reitit-ring "0.1.4-SNAPSHOT"]
+[metosin/reitit-spec "0.1.4-SNAPSHOT"]
+[metosin/reitit-schema "0.1.4-SNAPSHOT"]
+[metosin/reitit-swagger "0.1.4-SNAPSHOT"]
+[metosin/reitit-swagger-ui "0.1.4-SNAPSHOT"]
```
## Quick start
diff --git a/doc/README.md b/doc/README.md
index 2d3079ff..d1fb5318 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -10,6 +10,7 @@
* Extendable
* Modular
* [Fast](performance.md)
+* [Frontend routing](./frontend/README.md)
Modules:
@@ -19,22 +20,24 @@ Modules:
* `reitit-schema` [Schema](https://github.com/plumatic/schema) coercion
* `reitit-swagger` [Swagger2](https://swagger.io/) apidocs
* `reitit-swagger-ui` Integrated [Swagger UI](https://github.com/swagger-api/swagger-ui).
+* `reitit-frontend` Tools for frontend routing.
-To use Reitit, add the following dependecy to your project:
+To use Reitit, add the following dependency to your project:
```clj
-[metosin/reitit "0.1.3"]
+[metosin/reitit "0.1.4-SNAPSHOT"]
```
Optionally, the parts can be required separately:
```clj
-[metosin/reitit-core "0.1.3"]
-[metosin/reitit-ring "0.1.3"]
-[metosin/reitit-spec "0.1.3"]
-[metosin/reitit-schema "0.1.3"]
-[metosin/reitit-swagger "0.1.3"]
-[metosin/reitit-swagger-ui "0.1.3"]
+[metosin/reitit-core "0.1.4-SNAPSHOT"]
+[metosin/reitit-ring "0.1.4-SNAPSHOT"]
+[metosin/reitit-spec "0.1.4-SNAPSHOT"]
+[metosin/reitit-schema "0.1.4-SNAPSHOT"]
+[metosin/reitit-swagger "0.1.4-SNAPSHOT"]
+[metosin/reitit-swagger-ui "0.1.4-SNAPSHOT"]
+[metosin/frontend "0.1.4-SNAPSHOT"]
```
For discussions, there is a [#reitit](https://clojurians.slack.com/messages/reitit/) channel in [Clojurians slack](http://clojurians.net/).
diff --git a/doc/cljdoc.edn b/doc/cljdoc.edn
index 819e60a8..ffc68d25 100644
--- a/doc/cljdoc.edn
+++ b/doc/cljdoc.edn
@@ -45,6 +45,11 @@
{:file "doc/ring/route_data_validation.md"}]
["Compiling Middleware" {:file "doc/ring/compiling_middleware.md"}]
["Swagger Support" {:file "doc/ring/swagger.md"}]]
+ ["Frontend"
+ {:file "doc/frontend/README.md"}
+ ["Basics" {:file "doc/frontend/basics.md"}]
+ ["Browser integration" {:file "doc/frontend/browser.md"}]
+ ["Controllers" {:file "doc/frontend/controllers/.md"}]]
["Performance" {:file "doc/performance.md"}]
["Interceptors (WIP)" {:file "doc/interceptors.md"}]
["Development Instructions" {:file "doc/development.md"}]
diff --git a/doc/frontend/README.md b/doc/frontend/README.md
new file mode 100644
index 00000000..65b78197
--- /dev/null
+++ b/doc/frontend/README.md
@@ -0,0 +1,5 @@
+# Frontend
+
+* [Basics](basics.md)
+* [Browser integration](browser.md)
+* [Controllers](controllers.md)
diff --git a/doc/frontend/basics.md b/doc/frontend/basics.md
new file mode 100644
index 00000000..b0d632fb
--- /dev/null
+++ b/doc/frontend/basics.md
@@ -0,0 +1,3 @@
+# Frontend basics
+
+TODO
diff --git a/doc/frontend/browser.md b/doc/frontend/browser.md
new file mode 100644
index 00000000..a7a8c613
--- /dev/null
+++ b/doc/frontend/browser.md
@@ -0,0 +1,3 @@
+# Frontend browser integration
+
+TODO
diff --git a/doc/frontend/controllers.md b/doc/frontend/controllers.md
new file mode 100644
index 00000000..11f9f263
--- /dev/null
+++ b/doc/frontend/controllers.md
@@ -0,0 +1,3 @@
+# Controllers
+
+TODO
diff --git a/doc/ring/ring.md b/doc/ring/ring.md
index b792c41c..1b916fd2 100644
--- a/doc/ring/ring.md
+++ b/doc/ring/ring.md
@@ -3,7 +3,7 @@
[Ring](https://github.com/ring-clojure/ring) is a Clojure web applications library inspired by Python's WSGI and Ruby's Rack. By abstracting the details of HTTP into a simple, unified API, Ring allows web applications to be constructed of modular components that can be shared among a variety of applications, web servers, and web frameworks.
```clj
-[metosin/reitit-ring "0.1.3"]
+[metosin/reitit-ring "0.1.4-SNAPSHOT"]
```
Ring-router adds support for [handlers](https://github.com/ring-clojure/ring/wiki/Concepts#handlers), [middleware](https://github.com/ring-clojure/ring/wiki/Concepts#middleware) and routing based on `:request-method`. Ring-router is created with `reitit.ring/router` function. It uses a custom route compiler, creating a optimized data structure for handling route matches, with compiled middleware chain & handlers for all request methods. It also ensures that all routes have a `:handler` defined. `reitit.ring/ring-handler` is used to create a Ring handler out of ring-router.
diff --git a/doc/ring/swagger.md b/doc/ring/swagger.md
index b3de46bf..243f4dc3 100644
--- a/doc/ring/swagger.md
+++ b/doc/ring/swagger.md
@@ -1,7 +1,7 @@
# Swagger Support
```
-[metosin/reitit-swagger "0.1.3"]
+[metosin/reitit-swagger "0.1.4-SNAPSHOT"]
```
Reitit supports [Swagger2](https://swagger.io/) documentation, thanks to [schema-tools](https://github.com/metosin/schema-tools) and [spec-tools](https://github.com/metosin/spec-tools). Documentation is extracted from route definitions, coercion `:parameters` and `:responses` and from a set of new documentation keys.
@@ -44,7 +44,7 @@ If you need to post-process the generated spec, just wrap the handler with a cus
[Swagger-ui](https://github.com/swagger-api/swagger-ui) is a user interface to visualize and interact with the Swagger specification. To make things easy, there is a pre-integrated version of the swagger-ui as a separate module.
```
-[metosin/reitit-swagger-ui "0.1.3"]
+[metosin/reitit-swagger-ui "0.1.4-SNAPSHOT"]
```
`reitit.swagger-ui/create-swagger-ui-hander` can be used to create a ring-handler to serve the swagger-ui. It accepts the following options:
diff --git a/examples/frontend-controllers/checkouts/reitit-core b/examples/frontend-controllers/checkouts/reitit-core
new file mode 120000
index 00000000..a59d247e
--- /dev/null
+++ b/examples/frontend-controllers/checkouts/reitit-core
@@ -0,0 +1 @@
+../../../modules/reitit-core
\ No newline at end of file
diff --git a/examples/frontend-controllers/checkouts/reitit-frontend b/examples/frontend-controllers/checkouts/reitit-frontend
new file mode 120000
index 00000000..20cdd448
--- /dev/null
+++ b/examples/frontend-controllers/checkouts/reitit-frontend
@@ -0,0 +1 @@
+../../../modules/reitit-frontend
\ No newline at end of file
diff --git a/examples/frontend-controllers/checkouts/reitit-schema b/examples/frontend-controllers/checkouts/reitit-schema
new file mode 120000
index 00000000..a68c7f05
--- /dev/null
+++ b/examples/frontend-controllers/checkouts/reitit-schema
@@ -0,0 +1 @@
+../../../modules/reitit-schema
\ No newline at end of file
diff --git a/examples/frontend-controllers/project.clj b/examples/frontend-controllers/project.clj
new file mode 100644
index 00000000..c040a046
--- /dev/null
+++ b/examples/frontend-controllers/project.clj
@@ -0,0 +1,52 @@
+(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.339"]
+ [metosin/reitit "0.1.4-SNAPSHOT"]
+ [metosin/reitit-schema "0.1.4-SNAPSHOT"]
+ [metosin/reitit-frontend "0.1.4-SNAPSHOT"]
+ ;; Just for pretty printting the match
+ [fipp "0.6.12"]]
+
+ :plugins [[lein-cljsbuild "1.1.7"]
+ [lein-figwheel "0.5.16"]]
+
+ :source-paths []
+ :resource-paths ["resources" "target/cljsbuild"]
+
+ :profiles {:dev {:dependencies [[binaryage/devtools "0.9.10"]]}}
+
+ :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})
diff --git a/examples/frontend-controllers/resources/public/index.html b/examples/frontend-controllers/resources/public/index.html
new file mode 100644
index 00000000..bbad514c
--- /dev/null
+++ b/examples/frontend-controllers/resources/public/index.html
@@ -0,0 +1,10 @@
+
+
+
+ Reitit frontend example
+
+
+
+
+
+
diff --git a/examples/frontend-controllers/src/frontend/core.cljs b/examples/frontend-controllers/src/frontend/core.cljs
new file mode 100644
index 00000000..960867a5
--- /dev/null
+++ b/examples/frontend-controllers/src/frontend/core.cljs
@@ -0,0 +1,95 @@
+(ns frontend.core
+ (:require [reagent.core :as r]
+ [reitit.core :as re]
+ [reitit.frontend :as rf]
+ [reitit.frontend.history :as rfh]
+ [reitit.frontend.controllers :as rfc]
+ [reitit.coercion :as rc]
+ [reitit.coercion.schema :as rsc]
+ [schema.core :as s]
+ [fipp.edn :as fedn]))
+
+(defonce history (atom nil))
+
+(defn home-page []
+ [:div
+ [:h2 "Welcome to frontend"]
+ [:p "Look at console log for controller calls."]])
+
+(defn item-page [match]
+ (let [{:keys [path query]} (:parameters match)
+ {:keys [id]} path]
+ [:div
+ [:ul
+ [:li [:a {:href (rfh/href @history ::item {:id 1})} "Item 1"]]
+ [:li [:a {:href (rfh/href @history ::item {:id 2} {:foo "bar"})} "Item 2"]]]
+ (if id
+ [:h2 "Selected item " id])
+ (if (:foo query)
+ [:p "Optional foo query param: " (:foo query)])]))
+
+(defonce match (r/atom nil))
+
+(defn current-page []
+ [:div
+ [:ul
+ [:li [:a {:href (rfh/href @history ::frontpage)} "Frontpage"]]
+ [:li
+ [:a {:href (rfh/href @history ::item-list)} "Item list"]
+ ]]
+ (if @match
+ (let [view (:view (:data @match))]
+ [view @match]))
+ [:pre (with-out-str (fedn/pprint @match))]])
+
+(defn log-fn [& params]
+ (fn [_]
+ (apply js/console.log params)))
+
+(def routes
+ (re/router
+ ["/"
+ [""
+ {:name ::frontpage
+ :view home-page
+ :controllers [{:start (log-fn "start" "frontpage controller")
+ :stop (log-fn "stop" "frontpage controller")}]}]
+ ["items"
+ ;; Shared data for sub-routes
+ {:view item-page
+ :controllers [{:start (log-fn "start" "items controller")
+ :stop (log-fn "stop" "items controller")}]}
+
+ [""
+ {:name ::item-list
+ :controllers [{:start (log-fn "start" "item-list controller")
+ :stop (log-fn "stop" "item-list controller")}]}]
+ ["/:id"
+ {:name ::item
+ :parameters {:path {:id s/Int}
+ :query {(s/optional-key :foo) s/Keyword}}
+ :controllers [{:params (fn [match]
+ (:path (:parameters match)))
+ :start (fn [params]
+ (js/console.log "start" "item controller" (:id params)))
+ :stop (fn [params]
+ (js/console.log "stop" "item controller" (:id params)))}]}]]]
+ {:compile rc/compile-request-coercers
+ :data {:controllers [{:start (log-fn "start" "root-controller")
+ :stop (log-fn "stop" "root controller")}]
+ :coercion rsc/coercion}}))
+
+(defn init! []
+ (swap! history (fn [old-history]
+ (rfh/stop! old-history)
+ (rfh/start!
+ routes
+ (fn [new-match]
+ (swap! match (fn [old-match]
+ (if new-match
+ (assoc new-match :controllers (rfc/apply-controllers (:controllers old-match) new-match))))))
+ {:use-fragment true
+ :path-prefix "/"})))
+ (r/render [current-page] (.getElementById js/document "app")))
+
+(init!)
diff --git a/examples/frontend/checkouts/reitit-core b/examples/frontend/checkouts/reitit-core
new file mode 120000
index 00000000..a59d247e
--- /dev/null
+++ b/examples/frontend/checkouts/reitit-core
@@ -0,0 +1 @@
+../../../modules/reitit-core
\ No newline at end of file
diff --git a/examples/frontend/checkouts/reitit-frontend b/examples/frontend/checkouts/reitit-frontend
new file mode 120000
index 00000000..20cdd448
--- /dev/null
+++ b/examples/frontend/checkouts/reitit-frontend
@@ -0,0 +1 @@
+../../../modules/reitit-frontend
\ No newline at end of file
diff --git a/examples/frontend/checkouts/reitit-schema b/examples/frontend/checkouts/reitit-schema
new file mode 120000
index 00000000..a68c7f05
--- /dev/null
+++ b/examples/frontend/checkouts/reitit-schema
@@ -0,0 +1 @@
+../../../modules/reitit-schema
\ No newline at end of file
diff --git a/examples/frontend/project.clj b/examples/frontend/project.clj
new file mode 100644
index 00000000..c040a046
--- /dev/null
+++ b/examples/frontend/project.clj
@@ -0,0 +1,52 @@
+(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.339"]
+ [metosin/reitit "0.1.4-SNAPSHOT"]
+ [metosin/reitit-schema "0.1.4-SNAPSHOT"]
+ [metosin/reitit-frontend "0.1.4-SNAPSHOT"]
+ ;; Just for pretty printting the match
+ [fipp "0.6.12"]]
+
+ :plugins [[lein-cljsbuild "1.1.7"]
+ [lein-figwheel "0.5.16"]]
+
+ :source-paths []
+ :resource-paths ["resources" "target/cljsbuild"]
+
+ :profiles {:dev {:dependencies [[binaryage/devtools "0.9.10"]]}}
+
+ :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})
diff --git a/examples/frontend/resources/public/index.html b/examples/frontend/resources/public/index.html
new file mode 100644
index 00000000..bbad514c
--- /dev/null
+++ b/examples/frontend/resources/public/index.html
@@ -0,0 +1,10 @@
+
+
+
+ Reitit frontend example
+
+
+
+
+
+
diff --git a/examples/frontend/src/frontend/core.cljs b/examples/frontend/src/frontend/core.cljs
new file mode 100644
index 00000000..f8c4f890
--- /dev/null
+++ b/examples/frontend/src/frontend/core.cljs
@@ -0,0 +1,73 @@
+(ns frontend.core
+ (:require [reagent.core :as r]
+ [reitit.core :as re]
+ [reitit.frontend :as rf]
+ [reitit.frontend.history :as rfh]
+ [reitit.coercion :as rc]
+ [reitit.coercion.schema :as rsc]
+ [schema.core :as s]
+ [fipp.edn :as fedn]))
+
+(defonce history (atom nil))
+
+(defn home-page []
+ [:div
+ [:h2 "Welcome to frontend"]])
+
+(defn about-page []
+ [:div
+ [:h2 "About frontend"]
+ [:ul
+ [:li [:a {:href "http://google.com"} "external link"]]
+ [:li [:a {:href (rfh/href @history ::foobar)} "Missing route"]]
+ [:li [:a {:href (rfh/href @history ::item)} "Missing route params"]]]])
+
+(defn item-page [match]
+ (let [{:keys [path query]} (:parameters match)
+ {:keys [id]} path]
+ [:div
+ [:h2 "Selected item " id]
+ (if (:foo query)
+ [:p "Optional foo query param: " (:foo query)])]))
+
+(defonce match (r/atom nil))
+
+(defn current-page []
+ [:div
+ [:ul
+ [:li [:a {:href (rfh/href @history ::frontpage)} "Frontpage"]]
+ [:li [:a {:href (rfh/href @history ::about)} "About"]]
+ [:li [:a {:href (rfh/href @history ::item {:id 1})} "Item 1"]]
+ [:li [:a {:href (rfh/href @history ::item {:id 2} {:foo "bar"})} "Item 2"]]]
+ (if @match
+ (let [view (:view (:data @match))]
+ [view @match]))
+ [:pre (with-out-str (fedn/pprint @match))]])
+
+(def routes
+ (re/router
+ ["/"
+ [""
+ {:name ::frontpage
+ :view home-page}]
+ ["about"
+ {:name ::about
+ :view about-page}]
+ ["item/:id"
+ {:name ::item
+ :view item-page
+ :parameters {:path {:id s/Int}
+ :query {(s/optional-key :foo) s/Keyword}}}]]
+ {:compile rc/compile-request-coercers
+ :data {:coercion rsc/coercion}}))
+
+(defn init! []
+ (swap! history (fn [old-history]
+ (rfh/stop! old-history)
+ (rfh/start! routes
+ (fn [m] (reset! match m))
+ {:use-fragment true
+ :path-prefix "/"})))
+ (r/render [current-page] (.getElementById js/document "app")))
+
+(init!)
diff --git a/examples/just-coercion-with-ring/project.clj b/examples/just-coercion-with-ring/project.clj
index 3801bb08..a1b18270 100644
--- a/examples/just-coercion-with-ring/project.clj
+++ b/examples/just-coercion-with-ring/project.clj
@@ -3,4 +3,4 @@
:dependencies [[org.clojure/clojure "1.9.0"]
[ring "1.6.3"]
[metosin/muuntaja "0.4.1"]
- [metosin/reitit "0.1.3"]])
+ [metosin/reitit "0.1.4-SNAPSHOT"]])
diff --git a/examples/ring-example/project.clj b/examples/ring-example/project.clj
index 95b9975e..25091829 100644
--- a/examples/ring-example/project.clj
+++ b/examples/ring-example/project.clj
@@ -3,4 +3,4 @@
:dependencies [[org.clojure/clojure "1.9.0"]
[ring "1.6.3"]
[metosin/muuntaja "0.4.1"]
- [metosin/reitit "0.1.3"]])
+ [metosin/reitit "0.1.4-SNAPSHOT"]])
diff --git a/examples/ring-swagger/project.clj b/examples/ring-swagger/project.clj
index bea72569..76887394 100644
--- a/examples/ring-swagger/project.clj
+++ b/examples/ring-swagger/project.clj
@@ -3,5 +3,5 @@
:dependencies [[org.clojure/clojure "1.9.0"]
[ring "1.6.3"]
[metosin/muuntaja "0.5.0"]
- [metosin/reitit "0.1.3"]]
+ [metosin/reitit "0.1.4-SNAPSHOT"]]
:repl-options {:init-ns example.server})
diff --git a/modules/reitit-core/project.clj b/modules/reitit-core/project.clj
index f34eb15b..fe168dc8 100644
--- a/modules/reitit-core/project.clj
+++ b/modules/reitit-core/project.clj
@@ -1,4 +1,4 @@
-(defproject metosin/reitit-core "0.1.3"
+(defproject metosin/reitit-core "0.1.4-SNAPSHOT"
:description "Snappy data-driven router for Clojure(Script)"
:url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License"
diff --git a/modules/reitit-core/src/reitit/impl.cljc b/modules/reitit-core/src/reitit/impl.cljc
index 003f2870..c5fca01e 100644
--- a/modules/reitit-core/src/reitit/impl.cljc
+++ b/modules/reitit-core/src/reitit/impl.cljc
@@ -131,7 +131,10 @@
(defn path-for [^Route route path-params]
(if-let [required (:path-params route)]
(if (every? #(contains? path-params %) required)
- (str "/" (str/join \/ (map #(get (or path-params {}) % %) (:path-parts route)))))
+ (->> (:path-parts route)
+ (map #(get (or path-params {}) % %))
+ (str/join \/)
+ (str "/")))
(:path route)))
(defn throw-on-missing-path-params [template required path-params]
@@ -214,5 +217,8 @@
"shallow transform of query parameters into query string"
[params]
(->> params
- (map (fn [[k v]] (str (url-encode (into-string k)) "=" (url-encode (into-string v)))))
+ (map (fn [[k v]]
+ (str (url-encode (into-string k))
+ "="
+ (url-encode (into-string v)))))
(str/join "&")))
diff --git a/modules/reitit-frontend/project.clj b/modules/reitit-frontend/project.clj
new file mode 100644
index 00000000..2d5b0034
--- /dev/null
+++ b/modules/reitit-frontend/project.clj
@@ -0,0 +1,9 @@
+(defproject metosin/reitit-frontend "0.1.4-SNAPSHOT"
+ :description "Reitit: Clojurescript frontend routing core"
+ :url "https://github.com/metosin/reitit"
+ :license {:name "Eclipse Public License"
+ :url "http://www.eclipse.org/legal/epl-v10.html"}
+ :plugins [[lein-parent "0.3.2"]]
+ :parent-project {:path "../../project.clj"
+ :inherit [:deploy-repositories :managed-dependencies]}
+ :dependencies [[metosin/reitit-core]])
diff --git a/modules/reitit-frontend/src/reitit/frontend.cljs b/modules/reitit-frontend/src/reitit/frontend.cljs
new file mode 100644
index 00000000..6cf14ed7
--- /dev/null
+++ b/modules/reitit-frontend/src/reitit/frontend.cljs
@@ -0,0 +1,63 @@
+(ns reitit.frontend
+ ""
+ (:require [reitit.core :as reitit]
+ [clojure.string :as str]
+ [clojure.set :as set]
+ [reitit.coercion :as coercion]
+ [goog.events :as e]
+ [goog.dom :as dom])
+ (:import goog.Uri))
+
+(defn query-params
+ "Given goog.Uri, read query parameters into Clojure map."
+ [^goog.Uri uri]
+ (let [q (.getQueryData uri)]
+ (->> q
+ (.getKeys)
+ (map (juxt keyword #(.get q %)))
+ (into {}))))
+
+(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 (:path-params match)})]
+ (assoc match :parameters parameters)))))
+
+(defn match-by-name
+ ([router name]
+ (match-by-name router name {}))
+ ([router name path-params]
+ (reitit/match-by-name router name path-params)))
+
+(defn match-by-name!
+ "Logs problems using console.warn"
+ ([router name]
+ (match-by-name! router name {}))
+ ([router name path-params]
+ (if-let [match (match-by-name router name path-params)]
+ (if (reitit/partial-match? match)
+ (if (every? #(contains? path-params %) (:required match))
+ match
+ (let [defined (-> path-params keys set)
+ missing (set/difference (:required match) defined)]
+ (js/console.warn
+ "missing path-params for route" name
+ {:template (:template match)
+ :missing missing
+ :path-params path-params
+ :required (:required match)})
+ nil))
+ match)
+ (do (js/console.warn "missing route" name)
+ nil))))
diff --git a/modules/reitit-frontend/src/reitit/frontend/controllers.cljs b/modules/reitit-frontend/src/reitit/frontend/controllers.cljs
new file mode 100644
index 00000000..8556314d
--- /dev/null
+++ b/modules/reitit-frontend/src/reitit/frontend/controllers.cljs
@@ -0,0 +1,40 @@
+(ns reitit.frontend.controllers)
+
+(defn- pad-same-length [a b]
+ (concat a (take (- (count b) (count a)) (repeat nil))))
+
+(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 apply-controllers
+ "Applies changes between current controllers and
+ those previously enabled. Resets controllers whose
+ parameters have changed."
+ [old-controllers new-match]
+ (let [new-controllers (mapv (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))
diff --git a/modules/reitit-frontend/src/reitit/frontend/history.cljs b/modules/reitit-frontend/src/reitit/frontend/history.cljs
new file mode 100644
index 00000000..7e178723
--- /dev/null
+++ b/modules/reitit-frontend/src/reitit/frontend/history.cljs
@@ -0,0 +1,127 @@
+(ns reitit.frontend.history
+ ""
+ (:require [reitit.core :as reitit]
+ [clojure.string :as string]
+ [goog.events :as e]
+ [goog.dom :as dom]
+ [reitit.core :as r]
+ [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)
+ ;; If no fragment at all, default to "/"
+ ;; If fragment is present, the token already is prefixed with "/"
+ (if (= "" token)
+ (.getPathPrefix 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]
+ (if token
+ (str (if (.-useFragment_ history)
+ (str "#"))
+ (.getPathPrefix history)
+ token)))
+
+(def ^:private current-domain (if (exists? js/location)
+ (.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!
+ "This registers event listeners on either haschange or HTML5 history.
+ When using with development workflow like Figwheel, rememeber to
+ remove listeners using stop! call before calling start! again.
+
+ 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)
+ (.dispose history))}))
+
+(defn stop! [{:keys [close-fn]}]
+ (if close-fn
+ (close-fn)))
+
+(defn- match->token [history match k params query]
+ (some->> (r/match->path match query)
+ (path->token history)))
+
+(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 query)]
+ (token->href history token))))
+
+(defn replace-token
+ ([state k params]
+ (replace-token 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 query)]
+ (.replaceToken history token))))
diff --git a/modules/reitit-ring/project.clj b/modules/reitit-ring/project.clj
index b933437c..ee10b9c1 100644
--- a/modules/reitit-ring/project.clj
+++ b/modules/reitit-ring/project.clj
@@ -1,4 +1,4 @@
-(defproject metosin/reitit-ring "0.1.3"
+(defproject metosin/reitit-ring "0.1.4-SNAPSHOT"
:description "Reitit: Ring routing"
:url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License"
diff --git a/modules/reitit-schema/project.clj b/modules/reitit-schema/project.clj
index fe554e2d..61652645 100644
--- a/modules/reitit-schema/project.clj
+++ b/modules/reitit-schema/project.clj
@@ -1,4 +1,4 @@
-(defproject metosin/reitit-schema "0.1.3"
+(defproject metosin/reitit-schema "0.1.4-SNAPSHOT"
:description "Reitit: Plumatic Schema coercion"
:url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License"
diff --git a/modules/reitit-spec/project.clj b/modules/reitit-spec/project.clj
index 0d1454b7..eeb7e735 100644
--- a/modules/reitit-spec/project.clj
+++ b/modules/reitit-spec/project.clj
@@ -1,4 +1,4 @@
-(defproject metosin/reitit-spec "0.1.3"
+(defproject metosin/reitit-spec "0.1.4-SNAPSHOT"
:description "Reitit: clojure.spec coercion"
:url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License"
diff --git a/modules/reitit-swagger-ui/project.clj b/modules/reitit-swagger-ui/project.clj
index 39dffa03..b9903351 100644
--- a/modules/reitit-swagger-ui/project.clj
+++ b/modules/reitit-swagger-ui/project.clj
@@ -1,4 +1,4 @@
-(defproject metosin/reitit-swagger-ui "0.1.3"
+(defproject metosin/reitit-swagger-ui "0.1.4-SNAPSHOT"
:description "Reitit: Swagger-ui support"
:url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License"
diff --git a/modules/reitit-swagger/project.clj b/modules/reitit-swagger/project.clj
index 3677678a..6522fac6 100644
--- a/modules/reitit-swagger/project.clj
+++ b/modules/reitit-swagger/project.clj
@@ -1,4 +1,4 @@
-(defproject metosin/reitit-swagger "0.1.3"
+(defproject metosin/reitit-swagger "0.1.4-SNAPSHOT"
:description "Reitit: Swagger-support"
:url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License"
diff --git a/modules/reitit/project.clj b/modules/reitit/project.clj
index 391b1dd2..176b6f89 100644
--- a/modules/reitit/project.clj
+++ b/modules/reitit/project.clj
@@ -1,4 +1,4 @@
-(defproject metosin/reitit "0.1.3"
+(defproject metosin/reitit "0.1.4-SNAPSHOT"
:description "Snappy data-driven router for Clojure(Script)"
:url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License"
@@ -11,4 +11,5 @@
[metosin/reitit-spec]
[metosin/reitit-schema]
[metosin/reitit-swagger]
- [metosin/reitit-swagger-ui]])
+ [metosin/reitit-swagger-ui]
+ [metosin/reitit-frontend]])
diff --git a/project.clj b/project.clj
index 905bd954..783f9a81 100644
--- a/project.clj
+++ b/project.clj
@@ -1,4 +1,4 @@
-(defproject metosin/reitit-parent "0.1.3"
+(defproject metosin/reitit-parent "0.1.4-SNAPSHOT"
:description "Snappy data-driven router for Clojure(Script)"
:url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License"
@@ -9,13 +9,14 @@
:source-uri "https://github.com/metosin/reitit/{version}/{filepath}#L{line}"
:metadata {:doc/format :markdown}}
- :managed-dependencies [[metosin/reitit "0.1.3"]
- [metosin/reitit-core "0.1.3"]
- [metosin/reitit-ring "0.1.3"]
- [metosin/reitit-spec "0.1.3"]
- [metosin/reitit-schema "0.1.3"]
- [metosin/reitit-swagger "0.1.3"]
- [metosin/reitit-swagger-ui "0.1.3"]
+ :managed-dependencies [[metosin/reitit "0.1.4-SNAPSHOT"]
+ [metosin/reitit-core "0.1.4-SNAPSHOT"]
+ [metosin/reitit-ring "0.1.4-SNAPSHOT"]
+ [metosin/reitit-spec "0.1.4-SNAPSHOT"]
+ [metosin/reitit-schema "0.1.4-SNAPSHOT"]
+ [metosin/reitit-swagger "0.1.4-SNAPSHOT"]
+ [metosin/reitit-swagger-ui "0.1.4-SNAPSHOT"]
+ [metosin/reitit-frontend "0.1.4-SNAPSHOT"]
[meta-merge "1.0.0"]
[ring/ring-core "1.6.3"]
@@ -40,10 +41,11 @@
"modules/reitit-spec/src"
"modules/reitit-schema/src"
"modules/reitit-swagger/src"
- "modules/reitit-swagger-ui/src"]
+ "modules/reitit-swagger-ui/src"
+ "modules/reitit-frontend/src"]
:dependencies [[org.clojure/clojure "1.9.0"]
- [org.clojure/clojurescript "1.10.238"]
+ [org.clojure/clojurescript "1.10.339"]
;; modules dependencies
[metosin/reitit]
@@ -60,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"]
diff --git a/scripts/lein-modules b/scripts/lein-modules
index daf815fe..fc3870d8 100755
--- a/scripts/lein-modules
+++ b/scripts/lein-modules
@@ -3,6 +3,6 @@
set -e
# Modules
-for ext in reitit-core reitit-ring reitit-spec reitit-schema reitit-swagger reitit-swagger-ui reitit; do
+for ext in reitit-core reitit-ring reitit-spec reitit-schema reitit-swagger reitit-swagger-ui reitit-frontend reitit; do
cd modules/$ext; lein "$@"; cd ../..;
done
diff --git a/test/cljs/reitit/doo_runner.cljs b/test/cljs/reitit/doo_runner.cljs
index 57b7dc2b..3fbc392b 100644
--- a/test/cljs/reitit/doo_runner.cljs
+++ b/test/cljs/reitit/doo_runner.cljs
@@ -5,7 +5,10 @@
reitit.impl-test
reitit.middleware-test
reitit.ring-test
- #_reitit.spec-test))
+ #_reitit.spec-test
+ reitit.frontend.core-test
+ reitit.frontend.history-test
+ reitit.frontend.controllers-test))
(enable-console-print!)
@@ -14,4 +17,7 @@
'reitit.impl-test
'reitit.middleware-test
'reitit.ring-test
- #_'reitit.spec-test)
+ #_'reitit.spec-test
+ 'reitit.frontend.core-test
+ 'reitit.frontend.history-test
+ 'reitit.frontend.controllers-test)
diff --git a/test/cljs/reitit/frontend/controllers_test.cljs b/test/cljs/reitit/frontend/controllers_test.cljs
new file mode 100644
index 00000000..b4d2238c
--- /dev/null
+++ b/test/cljs/reitit/frontend/controllers_test.cljs
@@ -0,0 +1,68 @@
+(ns reitit.frontend.controllers-test
+ (:require [clojure.test :refer [deftest testing is are]]
+ [reitit.frontend.controllers :as rfc]))
+
+(deftest apply-controller-test
+ (is (= :ok (rfc/apply-controller {:stop (fn [_] :ok)} :stop)))
+ (is (= :ok (rfc/apply-controller {:start (fn [_] :ok)} :start))))
+
+(deftest apply-controllers-test
+ (let [log (atom [])
+ controller-state (atom [])
+ controller-1 {:start (fn [_] (swap! log conj :start-1))
+ :stop (fn [_] (swap! log conj :stop-1))}
+ controller-2 {:start (fn [_] (swap! log conj :start-2))
+ :stop (fn [_] (swap! log conj :stop-2))}
+ controller-3 {:start (fn [{:keys [foo]}] (swap! log conj [:start-3 foo]))
+ :stop (fn [{:keys [foo]}] (swap! log conj [:stop-3 foo]))
+ :params (fn [match]
+ {:foo (-> match :parameters :path :foo)})}]
+
+ (testing "single controller started"
+ (swap! controller-state rfc/apply-controllers
+ {:data {:controllers [controller-1]}})
+
+ (is (= [:start-1] @log))
+ (is (= [(assoc controller-1 ::rfc/params nil)] @controller-state))
+ (reset! log []))
+
+ (testing "second controller started"
+ (swap! controller-state rfc/apply-controllers
+ {:data {:controllers [controller-1 controller-2]}})
+
+ (is (= [:start-2] @log))
+ (is (= [(assoc controller-1 ::rfc/params nil)
+ (assoc controller-2 ::rfc/params nil)]
+ @controller-state))
+ (reset! log []))
+
+ (testing "second controller replaced"
+ (swap! controller-state rfc/apply-controllers
+ {:data {:controllers [controller-1 controller-3]}
+ :parameters {:path {:foo 5}}})
+
+ (is (= [:stop-2 [:start-3 5]] @log))
+ (is (= [(assoc controller-1 ::rfc/params nil)
+ (assoc controller-3 ::rfc/params {:foo 5})]
+ @controller-state))
+ (reset! log []))
+
+ (testing "controller parameter changed"
+ (swap! controller-state rfc/apply-controllers
+ {:data {:controllers [controller-1 controller-3]}
+ :parameters {:path {:foo 1}}})
+
+ (is (= [[:stop-3 5] [:start-3 1]] @log))
+ (is (= [(assoc controller-1 ::rfc/params nil)
+ (assoc controller-3 ::rfc/params {:foo 1})]
+ @controller-state))
+ (reset! log []))
+
+ (testing "all controllers stopped"
+ (swap! controller-state rfc/apply-controllers
+ {:data {:controllers []}})
+
+ (is (= [:stop-1 [:stop-3 1]] @log))
+ (is (= [] @controller-state))
+ (reset! log []))
+ ))
diff --git a/test/cljs/reitit/frontend/core_test.cljs b/test/cljs/reitit/frontend/core_test.cljs
new file mode 100644
index 00000000..8a18f4d0
--- /dev/null
+++ b/test/cljs/reitit/frontend/core_test.cljs
@@ -0,0 +1,89 @@
+(ns reitit.frontend.core-test
+ (:require [clojure.test :refer [deftest testing is are]]
+ [reitit.core :as r]
+ [reitit.frontend :as rf]
+ [reitit.coercion :as rc]
+ [schema.core :as s]
+ [reitit.coercion.schema :as rsc]
+ [reitit.frontend.test-utils :refer [capture-console]]))
+
+(defn m [x]
+ (assoc x :data nil :result nil))
+
+(deftest match-by-path-test
+ (testing "simple"
+ (let [router (r/router ["/"
+ ["" ::frontpage]
+ ["foo" ::foo]
+ ["bar" ::bar]])]
+ (is (= (r/map->Match
+ {:template "/"
+ :data {:name ::frontpage}
+ :path-params {}
+ :path "/"
+ :parameters {:query {}
+ :path {}}})
+ (rf/match-by-path router "/")))
+
+ (is (= "/"
+ (r/match->path (rf/match-by-name router ::frontpage))))
+
+ (is (= (r/map->Match
+ {:template "/foo"
+ :data {:name ::foo}
+ :path-params {}
+ :path "/foo"
+ :parameters {:query {}
+ :path {}}})
+ (rf/match-by-path router "/foo")))
+
+ (is (= "/foo"
+ (r/match->path (rf/match-by-name router ::foo))))
+
+ (is (= [{:type :warn
+ :message ["missing route" ::asd]}]
+ (:messages
+ (capture-console
+ (fn []
+ (rf/match-by-name! router ::asd))))))))
+
+ (testing "schema coercion"
+ (let [router (r/router ["/"
+ [":id" {:name ::foo
+ :parameters {:path {:id s/Int}
+ :query {(s/optional-key :mode) s/Keyword}}}]]
+ {:compile rc/compile-request-coercers
+ :data {:coercion rsc/coercion}})]
+ (is (= (r/map->Match
+ {:template "/:id"
+ :path-params {:id "5"}
+ :path "/5"
+ :parameters {:query {}
+ :path {:id 5}}})
+ (m (rf/match-by-path router "/5"))))
+
+ (is (= "/5"
+ (r/match->path (rf/match-by-name router ::foo {:id 5}))))
+
+ (is (= (r/map->Match
+ {:template "/:id"
+ :path-params {:id "5"}
+ ;; Note: query not included in path
+ :path "/5"
+ :parameters {:path {:id 5}
+ :query {:mode :foo}}})
+ (m (rf/match-by-path router "/5?mode=foo"))))
+
+ (is (= "/5?mode=foo"
+ (r/match->path (rf/match-by-name router ::foo {:id 5}) {:mode :foo})))
+
+ (is (= [{:type :warn
+ :message ["missing path-params for route" ::foo
+ {:template "/:id"
+ :missing #{:id}
+ :required #{:id}
+ :path-params {}}]}]
+ (:messages
+ (capture-console
+ (fn []
+ (rf/match-by-name! router ::foo {})))))))))
diff --git a/test/cljs/reitit/frontend/history_test.cljs b/test/cljs/reitit/frontend/history_test.cljs
new file mode 100644
index 00000000..3185b611
--- /dev/null
+++ b/test/cljs/reitit/frontend/history_test.cljs
@@ -0,0 +1,59 @@
+(ns reitit.frontend.history-test
+ (:require [clojure.test :refer [deftest testing is are]]
+ [reitit.core :as r]
+ [reitit.frontend.history :as rfh]
+ [reitit.frontend.test-utils :refer [capture-console]]))
+
+(def browser (exists? js/window))
+
+(deftest fragment-history-test
+ (when browser
+ (let [router (r/router ["/"
+ ["" ::frontpage]
+ ["foo" ::foo]
+ ["bar/:id" ::bar]])
+ history (rfh/start! router
+ (fn [_])
+ {:use-fragment true
+ :path-prefix "/"})]
+
+ (testing "creating urls"
+ (is (= "#/foo"
+ (rfh/href history ::foo)))
+ (is (= "#/bar/5"
+ (rfh/href history ::bar {:id 5})))
+ (is (= "#/bar/5?q=x"
+ (rfh/href history ::bar {:id 5} {:q "x"})))
+ (let [{:keys [value messages]} (capture-console
+ (fn []
+ (rfh/href history ::asd)))]
+ (is (= nil value))
+ (is (= [{:type :warn
+ :message ["missing route" ::asd]}]
+ messages)))))))
+
+(deftest html5-history-test
+ (when browser
+ (let [router (r/router ["/"
+ ["" ::frontpage]
+ ["foo" ::foo]
+ ["bar/:id" ::bar]])
+ history (rfh/start! router
+ (fn [_])
+ {:use-fragment false
+ :path-prefix "/"})]
+
+ (testing "creating urls"
+ (is (= "/foo"
+ (rfh/href history ::foo)))
+ (is (= "/bar/5"
+ (rfh/href history ::bar {:id 5})))
+ (is (= "/bar/5?q=x"
+ (rfh/href history ::bar {:id 5} {:q "x"})))
+ (let [{:keys [value messages]} (capture-console
+ (fn []
+ (rfh/href history ::asd)))]
+ (is (= nil value))
+ (is (= [{:type :warn
+ :message ["missing route" ::asd]}]
+ messages)))))))
diff --git a/test/cljs/reitit/frontend/test_utils.cljs b/test/cljs/reitit/frontend/test_utils.cljs
new file mode 100644
index 00000000..b70c5f37
--- /dev/null
+++ b/test/cljs/reitit/frontend/test_utils.cljs
@@ -0,0 +1,15 @@
+(ns reitit.frontend.test-utils)
+
+(defn capture-console [f]
+ (let [messages (atom [])
+ original-console-warn js/console.warn
+ log (fn [t & message]
+ (swap! messages conj {:type t
+ :message message}))
+ value (try
+ (set! js/console.warn (partial log :warn))
+ (f)
+ (finally
+ (set! js/console.warn original-console-warn)))]
+ {:value value
+ :messages @messages}))