From 468a0947d2afe509c41100908ef8a947d381ba06 Mon Sep 17 00:00:00 2001 From: Juho Teperi Date: Fri, 12 Jan 2018 11:04:44 +0200 Subject: [PATCH 01/19] Frontend routing implementation --- .gitignore | 2 +- modules/reitit-frontend/project.clj | 9 ++ .../reitit-frontend/src/reitit/frontend.cljs | 90 +++++++++++++++++++ modules/reitit/project.clj | 4 +- project.clj | 4 +- scripts/lein-modules | 2 +- 6 files changed, 107 insertions(+), 4 deletions(-) create mode 100644 modules/reitit-frontend/project.clj create mode 100644 modules/reitit-frontend/src/reitit/frontend.cljs diff --git a/.gitignore b/.gitignore index 73f41df4..c65c7d42 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -/target +target/ /classes /checkouts pom.xml diff --git a/modules/reitit-frontend/project.clj b/modules/reitit-frontend/project.clj new file mode 100644 index 00000000..38c9557d --- /dev/null +++ b/modules/reitit-frontend/project.clj @@ -0,0 +1,9 @@ +(defproject metosin/reitit-frontend "0.1.3" + :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..a50c4e5f --- /dev/null +++ b/modules/reitit-frontend/src/reitit/frontend.cljs @@ -0,0 +1,90 @@ +(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 +;; + +(defn query-params + "Parse query-params from URL into a map." + [^goog.Uri uri] + (let [q (.getQueryData uri)] + (->> q + (.getKeys) + (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 #"/$" ""))) + +;; +;; Controller implementation +;; + +(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)) diff --git a/modules/reitit/project.clj b/modules/reitit/project.clj index 391b1dd2..1bca731f 100644 --- a/modules/reitit/project.clj +++ b/modules/reitit/project.clj @@ -11,4 +11,6 @@ [metosin/reitit-spec] [metosin/reitit-schema] [metosin/reitit-swagger] - [metosin/reitit-swagger-ui]]) + [metosin/reitit-swagger-ui] + [metosin/reitit-frontend] + [metosin/reitit-re-frame]]) diff --git a/project.clj b/project.clj index 905bd954..ab2f6e5a 100644 --- a/project.clj +++ b/project.clj @@ -16,6 +16,7 @@ [metosin/reitit-schema "0.1.3"] [metosin/reitit-swagger "0.1.3"] [metosin/reitit-swagger-ui "0.1.3"] + [metosin/reitit-frontend "0.1.3"] [meta-merge "1.0.0"] [ring/ring-core "1.6.3"] @@ -40,7 +41,8 @@ "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"] 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 From 417f35a31832c23dce1acf228f61c31fff1fc365 Mon Sep 17 00:00:00 2001 From: Juho Teperi Date: Fri, 8 Jun 2018 16:00:49 +0300 Subject: [PATCH 02/19] Create example --- .gitignore | 1 + examples/frontend/checkouts/reitit-core | 1 + examples/frontend/checkouts/reitit-frontend | 1 + examples/frontend/checkouts/reitit-schema | 1 + examples/frontend/project.clj | 51 ++++++++ examples/frontend/resources/public/index.html | 10 ++ examples/frontend/src/frontend/core.cljs | 50 ++++++++ .../reitit-frontend/src/reitit/frontend.cljs | 120 +++++++----------- .../src/reitit/frontend/history.cljs | 114 +++++++++++++++++ project.clj | 5 +- test/cljs/reitit/doo_runner.cljs | 6 +- test/cljs/reitit/frontend/core_test.cljs | 41 ++++++ 12 files changed, 322 insertions(+), 79 deletions(-) create mode 120000 examples/frontend/checkouts/reitit-core create mode 120000 examples/frontend/checkouts/reitit-frontend create mode 120000 examples/frontend/checkouts/reitit-schema create mode 100644 examples/frontend/project.clj create mode 100644 examples/frontend/resources/public/index.html create mode 100644 examples/frontend/src/frontend/core.cljs create mode 100644 modules/reitit-frontend/src/reitit/frontend/history.cljs create mode 100644 test/cljs/reitit/frontend/core_test.cljs diff --git a/.gitignore b/.gitignore index c65c7d42..7b183f21 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ pom.xml.asc /gh-pages /node_modules /_book +figwheel_server.log 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..821e45b9 --- /dev/null +++ b/examples/frontend/project.clj @@ -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"]]}}) 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..4f4d9276 --- /dev/null +++ b/examples/frontend/src/frontend/core.cljs @@ -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!) diff --git a/modules/reitit-frontend/src/reitit/frontend.cljs b/modules/reitit-frontend/src/reitit/frontend.cljs index a50c4e5f..ae2e5816 100644 --- a/modules/reitit-frontend/src/reitit/frontend.cljs +++ b/modules/reitit-frontend/src/reitit/frontend.cljs @@ -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)) 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..964a665a --- /dev/null +++ b/modules/reitit-frontend/src/reitit/frontend/history.cljs @@ -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))) diff --git a/project.clj b/project.clj index ab2f6e5a..c7172db6 100644 --- a/project.clj +++ b/project.clj @@ -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"] diff --git a/test/cljs/reitit/doo_runner.cljs b/test/cljs/reitit/doo_runner.cljs index 57b7dc2b..25f522c6 100644 --- a/test/cljs/reitit/doo_runner.cljs +++ b/test/cljs/reitit/doo_runner.cljs @@ -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) diff --git a/test/cljs/reitit/frontend/core_test.cljs b/test/cljs/reitit/frontend/core_test.cljs new file mode 100644 index 00000000..2d522a4b --- /dev/null +++ b/test/cljs/reitit/frontend/core_test.cljs @@ -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")))))) From 3611a1bafefb126c78c063af644fc5c55cbceb4a Mon Sep 17 00:00:00 2001 From: Juho Teperi Date: Tue, 12 Jun 2018 13:49:24 +0300 Subject: [PATCH 03/19] Fix frontend routing tests, add controller ns --- .../reitit-frontend/src/reitit/frontend.cljs | 2 +- .../src/reitit/frontend/controllers.cljs | 40 +++++++++++ test/cljs/reitit/doo_runner.cljs | 6 +- .../reitit/frontend/controllers_test.cljs | 68 +++++++++++++++++++ test/cljs/reitit/frontend/core_test.cljs | 58 +++++++++------- 5 files changed, 148 insertions(+), 26 deletions(-) create mode 100644 modules/reitit-frontend/src/reitit/frontend/controllers.cljs create mode 100644 test/cljs/reitit/frontend/controllers_test.cljs diff --git a/modules/reitit-frontend/src/reitit/frontend.cljs b/modules/reitit-frontend/src/reitit/frontend.cljs index ae2e5816..ba56ac12 100644 --- a/modules/reitit-frontend/src/reitit/frontend.cljs +++ b/modules/reitit-frontend/src/reitit/frontend.cljs @@ -43,7 +43,7 @@ (coercion/coerce-request (:result match) {:query-params q :path-params (:path-params match)}) {:query q - :path (:param match)})] + :path (:path-params match)})] (assoc match :parameters parameters))))) (defn match-by-name 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..fe97f675 --- /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 (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)) diff --git a/test/cljs/reitit/doo_runner.cljs b/test/cljs/reitit/doo_runner.cljs index 25f522c6..ce4d6ed7 100644 --- a/test/cljs/reitit/doo_runner.cljs +++ b/test/cljs/reitit/doo_runner.cljs @@ -6,7 +6,8 @@ reitit.middleware-test reitit.ring-test #_reitit.spec-test - reitit.frontend.core-test)) + reitit.frontend.core-test + reitit.frontend.controllers-test)) (enable-console-print!) @@ -16,4 +17,5 @@ 'reitit.middleware-test 'reitit.ring-test #_'reitit.spec-test - 'reitit.frontend.core-test) + 'reitit.frontend.core-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 index 2d522a4b..6e45ee38 100644 --- a/test/cljs/reitit/frontend/core_test.cljs +++ b/test/cljs/reitit/frontend/core_test.cljs @@ -4,7 +4,7 @@ [reitit.frontend :as rf] [reitit.coercion :as coercion] [schema.core :as s] - [reitit.coercion.schema :as schema])) + [reitit.coercion.schema :as schema-coercion])) (deftest match-by-path-test (testing "simple" @@ -12,30 +12,42 @@ ["" ::frontpage] ["foo" ::foo] ["bar" ::bar]])] - (is (= {:template "/" - :data {:name ::frontpage} - :path "/"} + (is (= (r/map->Match + {:template "/" + :data {:name ::frontpage} + :path-params {} + :path "/" + :parameters {:query {} + :path {}}}) (rf/match-by-path router "/"))) - (is (= {:template "/foo" - :data {:name ::foo} - :path "/foo"} + (is (= (r/map->Match + {:template "/foo" + :data {:name ::foo} + :path-params {} + :path "/foo" + :parameters {:query {} + :path {}}}) (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")))))) + [":id" {:name ::foo + :parameters {:path {:id s/Int} + :query {(s/optional-key :mode) s/Keyword}}}]] + {:compile coercion/compile-request-coercers + :data {:coercion schema-coercion/coercion}})] + (is (= (r/map->Match + {:template "/:id" + :path-params {:id "5"} + :path "/5" + :parameters {:query {} + :path {:id 5}}}) + (assoc (rf/match-by-path router "/5") :data nil :result nil))) + (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}}}) + (assoc (rf/match-by-path router "/5?mode=foo") :data nil :result nil)))))) From 6553795cb55ce9c4bc301e42376aea5db9662a86 Mon Sep 17 00:00:00 2001 From: Juho Teperi Date: Tue, 12 Jun 2018 13:58:43 +0300 Subject: [PATCH 04/19] Add some frontend history tests --- .../src/reitit/frontend/history.cljs | 10 +++-- test/cljs/reitit/doo_runner.cljs | 2 + test/cljs/reitit/frontend/history_test.cljs | 40 +++++++++++++++++++ 3 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 test/cljs/reitit/frontend/history_test.cljs diff --git a/modules/reitit-frontend/src/reitit/frontend/history.cljs b/modules/reitit-frontend/src/reitit/frontend/history.cljs index 964a665a..77db3406 100644 --- a/modules/reitit-frontend/src/reitit/frontend/history.cljs +++ b/modules/reitit-frontend/src/reitit/frontend/history.cljs @@ -93,10 +93,12 @@ (if close-fn (close-fn))) -(defn- match->token [history match k params] +(defn- match->token [history match k params query] ;; FIXME: query string (if-let [path (:path match)] - (path->token history path))) + (str (path->token history path) + (if query + (str "?" (rf/query-string query)))))) (defn href ([state k] @@ -105,10 +107,10 @@ (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 (match->token history match k params query)] (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)] + token (match->token history match k params query)] (.replaceToken history token))) diff --git a/test/cljs/reitit/doo_runner.cljs b/test/cljs/reitit/doo_runner.cljs index ce4d6ed7..3fbc392b 100644 --- a/test/cljs/reitit/doo_runner.cljs +++ b/test/cljs/reitit/doo_runner.cljs @@ -7,6 +7,7 @@ reitit.ring-test #_reitit.spec-test reitit.frontend.core-test + reitit.frontend.history-test reitit.frontend.controllers-test)) (enable-console-print!) @@ -18,4 +19,5 @@ 'reitit.ring-test #_'reitit.spec-test 'reitit.frontend.core-test + 'reitit.frontend.history-test 'reitit.frontend.controllers-test) diff --git a/test/cljs/reitit/frontend/history_test.cljs b/test/cljs/reitit/frontend/history_test.cljs new file mode 100644 index 00000000..6652f321 --- /dev/null +++ b/test/cljs/reitit/frontend/history_test.cljs @@ -0,0 +1,40 @@ +(ns reitit.frontend.history-test + (:require [clojure.test :refer [deftest testing is are]] + [reitit.core :as r] + [reitit.frontend.history :as rfh])) + +(deftest fragment-history-test + (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"})))))) + +(deftest html5-history-test + (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"})))))) From 50faacab2519e23cee70de76acd7a7d2a73285d2 Mon Sep 17 00:00:00 2001 From: Juho Teperi Date: Tue, 12 Jun 2018 14:09:55 +0300 Subject: [PATCH 05/19] Fix tests --- .../src/reitit/frontend/history.cljs | 20 +++++++++++-------- modules/reitit/project.clj | 3 +-- test/cljs/reitit/frontend/history_test.cljs | 8 ++++++-- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/modules/reitit-frontend/src/reitit/frontend/history.cljs b/modules/reitit-frontend/src/reitit/frontend/history.cljs index 77db3406..0ef39c2e 100644 --- a/modules/reitit-frontend/src/reitit/frontend/history.cljs +++ b/modules/reitit-frontend/src/reitit/frontend/history.cljs @@ -22,10 +22,11 @@ (count (.getPathPrefix history))))) (defn- token->href [history token] - (str (if (.-useFragment_ history) - (str "#")) - (.getPathPrefix history) - token)) + (if token + (str (if (.-useFragment_ history) + (str "#")) + (.getPathPrefix history) + token))) (def ^:private current-domain (.getDomain (.parse Uri js/location))) @@ -110,7 +111,10 @@ token (match->token history match k params query)] (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 query)] - (.replaceToken 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/project.clj b/modules/reitit/project.clj index 1bca731f..2915ef89 100644 --- a/modules/reitit/project.clj +++ b/modules/reitit/project.clj @@ -12,5 +12,4 @@ [metosin/reitit-schema] [metosin/reitit-swagger] [metosin/reitit-swagger-ui] - [metosin/reitit-frontend] - [metosin/reitit-re-frame]]) + [metosin/reitit-frontend]]) diff --git a/test/cljs/reitit/frontend/history_test.cljs b/test/cljs/reitit/frontend/history_test.cljs index 6652f321..4c09dcf1 100644 --- a/test/cljs/reitit/frontend/history_test.cljs +++ b/test/cljs/reitit/frontend/history_test.cljs @@ -19,7 +19,9 @@ (is (= "#/bar/5" (rfh/href history ::bar {:id 5}))) (is (= "#/bar/5?q=x" - (rfh/href history ::bar {:id 5} {:q "x"})))))) + (rfh/href history ::bar {:id 5} {:q "x"}))) + (is (= nil + (rfh/href history ::asd)))))) (deftest html5-history-test (let [router (r/router ["/" @@ -37,4 +39,6 @@ (is (= "/bar/5" (rfh/href history ::bar {:id 5}))) (is (= "/bar/5?q=x" - (rfh/href history ::bar {:id 5} {:q "x"})))))) + (rfh/href history ::bar {:id 5} {:q "x"}))) + (is (= nil + (rfh/href history ::asd)))))) From 61a9871ebbf8fb40fd57d42b03201b9bc1100629 Mon Sep 17 00:00:00 2001 From: Juho Teperi Date: Tue, 12 Jun 2018 15:44:37 +0300 Subject: [PATCH 06/19] Fix empty fragment url --- examples/frontend/src/frontend/core.cljs | 14 +++++--------- .../src/reitit/frontend/history.cljs | 6 +++++- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/examples/frontend/src/frontend/core.cljs b/examples/frontend/src/frontend/core.cljs index 4f4d9276..dee5c817 100644 --- a/examples/frontend/src/frontend/core.cljs +++ b/examples/frontend/src/frontend/core.cljs @@ -28,17 +28,13 @@ (def routes (rc/router - ["" + ["/" ["" - {:name ::frontpage-root + {:name ::frontpage :view home-page}] - ["/" - ["" - {:name ::frontpage - :view home-page}] - ["about" - {:name ::about - :view about-page}]]])) + ["about" + {:name ::about + :view about-page}]])) (defn init! [] (reset! router (rfh/start! routes diff --git a/modules/reitit-frontend/src/reitit/frontend/history.cljs b/modules/reitit-frontend/src/reitit/frontend/history.cljs index 0ef39c2e..e451d212 100644 --- a/modules/reitit-frontend/src/reitit/frontend/history.cljs +++ b/modules/reitit-frontend/src/reitit/frontend/history.cljs @@ -13,7 +13,11 @@ (defn- token->path [history token] (if (.-useFragment_ history) - token + ;; 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] From 7f1b2ce417b32ee925fa940a8a4d35535603c48a Mon Sep 17 00:00:00 2001 From: Juho Teperi Date: Wed, 11 Jul 2018 09:22:13 +0300 Subject: [PATCH 07/19] Check if js/location exists --- modules/reitit-frontend/src/reitit/frontend/history.cljs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/reitit-frontend/src/reitit/frontend/history.cljs b/modules/reitit-frontend/src/reitit/frontend/history.cljs index e451d212..5bcbf9d7 100644 --- a/modules/reitit-frontend/src/reitit/frontend/history.cljs +++ b/modules/reitit-frontend/src/reitit/frontend/history.cljs @@ -32,7 +32,8 @@ (.getPathPrefix history) token))) -(def ^:private current-domain (.getDomain (.parse Uri js/location))) +(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 From 434bc25cd6c6eff97888ff7a6fabaa03413d7648 Mon Sep 17 00:00:00 2001 From: Juho Teperi Date: Wed, 11 Jul 2018 09:39:10 +0300 Subject: [PATCH 08/19] Format code --- modules/reitit-core/src/reitit/impl.cljc | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) 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 "&"))) From 245902bd4dfd16cb0308483cd36244d8aab31246 Mon Sep 17 00:00:00 2001 From: Juho Teperi Date: Wed, 11 Jul 2018 09:52:35 +0300 Subject: [PATCH 09/19] Use query-string fn in core --- .../reitit-frontend/src/reitit/frontend.cljs | 26 +++---------------- .../src/reitit/frontend/history.cljs | 8 +++--- test/cljs/reitit/frontend/core_test.cljs | 24 ++++++++++++++--- 3 files changed, 28 insertions(+), 30 deletions(-) diff --git a/modules/reitit-frontend/src/reitit/frontend.cljs b/modules/reitit-frontend/src/reitit/frontend.cljs index ba56ac12..b4eb094e 100644 --- a/modules/reitit-frontend/src/reitit/frontend.cljs +++ b/modules/reitit-frontend/src/reitit/frontend.cljs @@ -16,19 +16,6 @@ (map (juxt keyword #(.get q %))) (into {})))) -(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))) - (defn match-by-path "Given routing tree and current path, return match with possibly coerced parameters. Return nil if no match found." @@ -47,12 +34,7 @@ (assoc match :parameters parameters))))) (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)) + ([router name] + (match-by-name router name {})) + ([router name path-params] + (reitit/match-by-name router name path-params))) diff --git a/modules/reitit-frontend/src/reitit/frontend/history.cljs b/modules/reitit-frontend/src/reitit/frontend/history.cljs index 5bcbf9d7..a95aa617 100644 --- a/modules/reitit-frontend/src/reitit/frontend/history.cljs +++ b/modules/reitit-frontend/src/reitit/frontend/history.cljs @@ -4,6 +4,7 @@ [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)) @@ -100,11 +101,8 @@ (close-fn))) (defn- match->token [history match k params query] - ;; FIXME: query string - (if-let [path (:path match)] - (str (path->token history path) - (if query - (str "?" (rf/query-string query)))))) + (some->> (r/match->path match query) + (path->token history))) (defn href ([state k] diff --git a/test/cljs/reitit/frontend/core_test.cljs b/test/cljs/reitit/frontend/core_test.cljs index 6e45ee38..48ee7384 100644 --- a/test/cljs/reitit/frontend/core_test.cljs +++ b/test/cljs/reitit/frontend/core_test.cljs @@ -6,6 +6,9 @@ [schema.core :as s] [reitit.coercion.schema :as schema-coercion])) +(defn m [x] + (assoc x :data nil :result nil)) + (deftest match-by-path-test (testing "simple" (let [router (r/router ["/" @@ -20,6 +23,10 @@ :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} @@ -27,7 +34,11 @@ :path "/foo" :parameters {:query {} :path {}}}) - (rf/match-by-path router "/foo"))))) + (rf/match-by-path router "/foo"))) + + + (is (= "/foo" + (r/match->path (rf/match-by-name router ::foo)))))) (testing "schema coercion" (let [router (r/router ["/" @@ -42,7 +53,11 @@ :path "/5" :parameters {:query {} :path {:id 5}}}) - (assoc (rf/match-by-path router "/5") :data nil :result nil))) + (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"} @@ -50,4 +65,7 @@ :path "/5" :parameters {:path {:id 5} :query {:mode :foo}}}) - (assoc (rf/match-by-path router "/5?mode=foo") :data nil :result nil)))))) + (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})))))) From 06b641398af282320187c10c8e1e9788426e602d Mon Sep 17 00:00:00 2001 From: Juho Teperi Date: Wed, 11 Jul 2018 09:52:45 +0300 Subject: [PATCH 10/19] Run history tests only on browser --- test/cljs/reitit/frontend/history_test.cljs | 60 +++++++++++---------- 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/test/cljs/reitit/frontend/history_test.cljs b/test/cljs/reitit/frontend/history_test.cljs index 4c09dcf1..a74882fa 100644 --- a/test/cljs/reitit/frontend/history_test.cljs +++ b/test/cljs/reitit/frontend/history_test.cljs @@ -3,42 +3,46 @@ [reitit.core :as r] [reitit.frontend.history :as rfh])) +(def browser (exists? js/window)) + (deftest fragment-history-test - (let [router (r/router ["/" + (when browser + (let [router (r/router ["/" ["" ::frontpage] ["foo" ::foo] ["bar/:id" ::bar]]) - history (rfh/start! router - (fn [_]) - {:use-fragment true - :path-prefix "/"})] + 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"}))) - (is (= nil - (rfh/href history ::asd)))))) + (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"}))) + (is (= nil + (rfh/href history ::asd))))))) (deftest html5-history-test - (let [router (r/router ["/" + (when browser + (let [router (r/router ["/" ["" ::frontpage] ["foo" ::foo] ["bar/:id" ::bar]]) - history (rfh/start! router - (fn [_]) - {:use-fragment false - :path-prefix "/"})] + 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"}))) - (is (= nil - (rfh/href history ::asd)))))) + (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"}))) + (is (= nil + (rfh/href history ::asd))))))) From 264981fd3d99c3474991839e4d7e6aca9ed12c44 Mon Sep 17 00:00:00 2001 From: Juho Teperi Date: Wed, 11 Jul 2018 09:57:00 +0300 Subject: [PATCH 11/19] Start version 0.1.4 --- README.md | 14 +++++++------- doc/README.md | 14 +++++++------- doc/ring/ring.md | 2 +- doc/ring/swagger.md | 4 ++-- examples/frontend/project.clj | 6 +++--- examples/just-coercion-with-ring/project.clj | 2 +- examples/ring-example/project.clj | 2 +- examples/ring-swagger/project.clj | 2 +- modules/reitit-core/project.clj | 2 +- modules/reitit-frontend/project.clj | 2 +- modules/reitit-ring/project.clj | 2 +- modules/reitit-schema/project.clj | 2 +- modules/reitit-spec/project.clj | 2 +- modules/reitit-swagger-ui/project.clj | 2 +- modules/reitit-swagger/project.clj | 2 +- modules/reitit/project.clj | 2 +- project.clj | 18 +++++++++--------- 17 files changed, 40 insertions(+), 40 deletions(-) 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..382b4b0c 100644 --- a/doc/README.md +++ b/doc/README.md @@ -23,18 +23,18 @@ Modules: To use Reitit, add the following dependecy 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"] ``` For discussions, there is a [#reitit](https://clojurians.slack.com/messages/reitit/) channel in [Clojurians slack](http://clojurians.net/). 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/project.clj b/examples/frontend/project.clj index 821e45b9..51aa1dfe 100644 --- a/examples/frontend/project.clj +++ b/examples/frontend/project.clj @@ -11,9 +11,9 @@ [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"]] + [metosin/reitit "0.1.4-SNAPSHOT"] + [metosin/reitit-schema "0.1.4-SNAPSHOT"] + [metosin/reitit-frontend "0.1.4-SNAPSHOT"]] :plugins [[lein-cljsbuild "1.1.7"]] 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-frontend/project.clj b/modules/reitit-frontend/project.clj index 38c9557d..2d5b0034 100644 --- a/modules/reitit-frontend/project.clj +++ b/modules/reitit-frontend/project.clj @@ -1,4 +1,4 @@ -(defproject metosin/reitit-frontend "0.1.3" +(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" 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 2915ef89..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" diff --git a/project.clj b/project.clj index c7172db6..38d869e4 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,14 +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"] - [metosin/reitit-frontend "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"] From dff920621087de0b26e0140133319bd4cd130525 Mon Sep 17 00:00:00 2001 From: Juho Teperi Date: Wed, 11 Jul 2018 09:57:12 +0300 Subject: [PATCH 12/19] Bump clojurescript --- examples/frontend/project.clj | 2 +- project.clj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/frontend/project.clj b/examples/frontend/project.clj index 51aa1dfe..a45dd1ab 100644 --- a/examples/frontend/project.clj +++ b/examples/frontend/project.clj @@ -10,7 +10,7 @@ [ring "1.6.3"] [compojure "1.6.1"] [hiccup "1.0.5"] - [org.clojure/clojurescript "1.10.238" :scope "provided"] + [org.clojure/clojurescript "1.10.339" :scope "provided"] [metosin/reitit "0.1.4-SNAPSHOT"] [metosin/reitit-schema "0.1.4-SNAPSHOT"] [metosin/reitit-frontend "0.1.4-SNAPSHOT"]] diff --git a/project.clj b/project.clj index 38d869e4..783f9a81 100644 --- a/project.clj +++ b/project.clj @@ -45,7 +45,7 @@ "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] From aeadfad880527e8eb76c0d07cdbadedbf925d19f Mon Sep 17 00:00:00 2001 From: Juho Teperi Date: Wed, 11 Jul 2018 10:26:53 +0300 Subject: [PATCH 13/19] Add path and query params to frontend example --- examples/frontend/project.clj | 4 ++- examples/frontend/src/frontend/core.cljs | 42 ++++++++++++++++++------ test/cljs/reitit/frontend/core_test.cljs | 8 ++--- 3 files changed, 39 insertions(+), 15 deletions(-) diff --git a/examples/frontend/project.clj b/examples/frontend/project.clj index a45dd1ab..db9991bc 100644 --- a/examples/frontend/project.clj +++ b/examples/frontend/project.clj @@ -13,7 +13,9 @@ [org.clojure/clojurescript "1.10.339" :scope "provided"] [metosin/reitit "0.1.4-SNAPSHOT"] [metosin/reitit-schema "0.1.4-SNAPSHOT"] - [metosin/reitit-frontend "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"]] diff --git a/examples/frontend/src/frontend/core.cljs b/examples/frontend/src/frontend/core.cljs index dee5c817..367ff97d 100644 --- a/examples/frontend/src/frontend/core.cljs +++ b/examples/frontend/src/frontend/core.cljs @@ -1,40 +1,62 @@ (ns frontend.core (:require [reagent.core :as r] - [reitit.core :as rc] + [reitit.core :as re] [reitit.frontend :as rf] [reitit.frontend.history :as rfh] - [reitit.coercion.schema :as rs])) + [reitit.coercion :as rc] + [reitit.coercion.schema :as rsc] + [schema.core :as s] + [fipp.edn :as fedn])) (def router (atom nil)) (defn home-page [] [:div - [:h2 "Welcome to frontend"] - [:div [:a {:href (rfh/href @router ::about)} "go to about page"]]]) + [:h2 "Welcome to frontend"]]) (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"]]]) + [:a {:href "http://google.com"} "external link"]]) + +(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 @router ::frontpage)} "Frontpage"]] + [:li [:a {:href (rfh/href @router ::about)} "About"]] + [:li [:a {:href (rfh/href @router ::item {:id 1})} "Item 1"]] + [:li [:a {:href (rfh/href @router ::item {:id 2} {:foo "bar"})} "Item 2"]]] (if @match - [:div [(:view (:data @match))]]) - (pr-str @match)]) + (let [view (:view (:data @match))] + [view @match])) + [:pre (with-out-str (fedn/pprint @match))]]) (def routes - (rc/router + (re/router ["/" ["" {:name ::frontpage :view home-page}] ["about" {:name ::about - :view about-page}]])) + :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! [] (reset! router (rfh/start! routes diff --git a/test/cljs/reitit/frontend/core_test.cljs b/test/cljs/reitit/frontend/core_test.cljs index 48ee7384..36d397c4 100644 --- a/test/cljs/reitit/frontend/core_test.cljs +++ b/test/cljs/reitit/frontend/core_test.cljs @@ -2,9 +2,9 @@ (:require [clojure.test :refer [deftest testing is are]] [reitit.core :as r] [reitit.frontend :as rf] - [reitit.coercion :as coercion] + [reitit.coercion :as rc] [schema.core :as s] - [reitit.coercion.schema :as schema-coercion])) + [reitit.coercion.schema :as rsc])) (defn m [x] (assoc x :data nil :result nil)) @@ -45,8 +45,8 @@ [":id" {:name ::foo :parameters {:path {:id s/Int} :query {(s/optional-key :mode) s/Keyword}}}]] - {:compile coercion/compile-request-coercers - :data {:coercion schema-coercion/coercion}})] + {:compile rc/compile-request-coercers + :data {:coercion rsc/coercion}})] (is (= (r/map->Match {:template "/:id" :path-params {:id "5"} From 358fbc19c6c552334f3a1f02adaf2f18f05c6c8a Mon Sep 17 00:00:00 2001 From: Juho Teperi Date: Wed, 11 Jul 2018 10:31:12 +0300 Subject: [PATCH 14/19] Clean frontend example project.clj --- examples/frontend/project.clj | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/examples/frontend/project.clj b/examples/frontend/project.clj index db9991bc..c040a046 100644 --- a/examples/frontend/project.clj +++ b/examples/frontend/project.clj @@ -10,18 +10,21 @@ [ring "1.6.3"] [compojure "1.6.1"] [hiccup "1.0.5"] - [org.clojure/clojurescript "1.10.339" :scope "provided"] + [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"]] + :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" @@ -44,10 +47,6 @@ :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"]]}}) + :figwheel {:http-server-root "public" + :server-port 3449 + :nrepl-port 7002}) From ae109e5350a0086e3b1b378de9fe92c744706096 Mon Sep 17 00:00:00 2001 From: Juho Teperi Date: Wed, 11 Jul 2018 12:39:10 +0300 Subject: [PATCH 15/19] Log warnings about missing route or params --- examples/frontend/src/frontend/core.cljs | 5 +++- .../reitit-frontend/src/reitit/frontend.cljs | 23 +++++++++++++++++++ .../src/reitit/frontend/history.cljs | 4 ++-- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/examples/frontend/src/frontend/core.cljs b/examples/frontend/src/frontend/core.cljs index 367ff97d..82731258 100644 --- a/examples/frontend/src/frontend/core.cljs +++ b/examples/frontend/src/frontend/core.cljs @@ -17,7 +17,10 @@ (defn about-page [] [:div [:h2 "About frontend"] - [:a {:href "http://google.com"} "external link"]]) + [:ul + [:li [:a {:href "http://google.com"} "external link"]] + [:li [:a {:href (rfh/href @router ::foobar)} "Missing route"]] + [:li [:a {:href (rfh/href @router ::item)} "Missing route params"]]]]) (defn item-page [match] (let [{:keys [path query]} (:parameters match) diff --git a/modules/reitit-frontend/src/reitit/frontend.cljs b/modules/reitit-frontend/src/reitit/frontend.cljs index b4eb094e..fba8fa3d 100644 --- a/modules/reitit-frontend/src/reitit/frontend.cljs +++ b/modules/reitit-frontend/src/reitit/frontend.cljs @@ -2,6 +2,7 @@ "" (: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]) @@ -38,3 +39,25 @@ (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 (reitit/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/history.cljs b/modules/reitit-frontend/src/reitit/frontend/history.cljs index a95aa617..97c135b1 100644 --- a/modules/reitit-frontend/src/reitit/frontend/history.cljs +++ b/modules/reitit-frontend/src/reitit/frontend/history.cljs @@ -110,7 +110,7 @@ ([state k params] (href state k params nil)) ([{:keys [router history]} k params query] - (let [match (rf/match-by-name router k params) + (let [match (rf/match-by-name! router k params) token (match->token history match k params query)] (token->href history token)))) @@ -118,6 +118,6 @@ ([state k params] (replace-token state k params nil)) ([{:keys [router history]} k params query] - (let [match (rf/match-by-name router k params) + (let [match (rf/match-by-name! router k params) token (match->token history match k params query)] (.replaceToken history token)))) From 8971c8fd2b5f11b2cf6018da011c319d62f21c8d Mon Sep 17 00:00:00 2001 From: Juho Teperi Date: Wed, 11 Jul 2018 12:55:16 +0300 Subject: [PATCH 16/19] Add tests for missing route and params warnings --- .../reitit-frontend/src/reitit/frontend.cljs | 2 +- test/cljs/reitit/frontend/core_test.cljs | 26 ++++++++++++++++--- test/cljs/reitit/frontend/history_test.cljs | 21 +++++++++++---- test/cljs/reitit/frontend/test_utils.cljs | 19 ++++++++++++++ 4 files changed, 58 insertions(+), 10 deletions(-) create mode 100644 test/cljs/reitit/frontend/test_utils.cljs diff --git a/modules/reitit-frontend/src/reitit/frontend.cljs b/modules/reitit-frontend/src/reitit/frontend.cljs index fba8fa3d..6cf14ed7 100644 --- a/modules/reitit-frontend/src/reitit/frontend.cljs +++ b/modules/reitit-frontend/src/reitit/frontend.cljs @@ -45,7 +45,7 @@ ([router name] (match-by-name! router name {})) ([router name path-params] - (if-let [match (reitit/match-by-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 diff --git a/test/cljs/reitit/frontend/core_test.cljs b/test/cljs/reitit/frontend/core_test.cljs index 36d397c4..8a18f4d0 100644 --- a/test/cljs/reitit/frontend/core_test.cljs +++ b/test/cljs/reitit/frontend/core_test.cljs @@ -4,7 +4,8 @@ [reitit.frontend :as rf] [reitit.coercion :as rc] [schema.core :as s] - [reitit.coercion.schema :as rsc])) + [reitit.coercion.schema :as rsc] + [reitit.frontend.test-utils :refer [capture-console]])) (defn m [x] (assoc x :data nil :result nil)) @@ -36,9 +37,15 @@ :path {}}}) (rf/match-by-path router "/foo"))) - (is (= "/foo" - (r/match->path (rf/match-by-name router ::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 ["/" @@ -68,4 +75,15 @@ (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})))))) + (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 index a74882fa..3185b611 100644 --- a/test/cljs/reitit/frontend/history_test.cljs +++ b/test/cljs/reitit/frontend/history_test.cljs @@ -1,7 +1,8 @@ (ns reitit.frontend.history-test (:require [clojure.test :refer [deftest testing is are]] [reitit.core :as r] - [reitit.frontend.history :as rfh])) + [reitit.frontend.history :as rfh] + [reitit.frontend.test-utils :refer [capture-console]])) (def browser (exists? js/window)) @@ -23,8 +24,13 @@ (rfh/href history ::bar {:id 5}))) (is (= "#/bar/5?q=x" (rfh/href history ::bar {:id 5} {:q "x"}))) - (is (= nil - (rfh/href history ::asd))))))) + (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 @@ -44,5 +50,10 @@ (rfh/href history ::bar {:id 5}))) (is (= "/bar/5?q=x" (rfh/href history ::bar {:id 5} {:q "x"}))) - (is (= nil - (rfh/href history ::asd))))))) + (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..8f57f549 --- /dev/null +++ b/test/cljs/reitit/frontend/test_utils.cljs @@ -0,0 +1,19 @@ +(ns reitit.frontend.test-utils) + +(defn capture-console [f] + (let [messages (atom []) + original-console js/console + log (fn [t & message] + (swap! messages conj {:type t + :message message})) + value (try + (set! js/console #js {:log (partial log :log) + :warn (partial log :warn) + :info (partial log :info) + :error (partial log :error) + :debug (partial log :debug)}) + (f) + (finally + (set! js/console original-console)))] + {:value value + :messages @messages})) From 82f8aaa8cfc71cc37d5a1069f4ecdf2d46afb99e Mon Sep 17 00:00:00 2001 From: Juho Teperi Date: Wed, 11 Jul 2018 13:19:24 +0300 Subject: [PATCH 17/19] Add frontend-controllers example --- .../checkouts/reitit-core | 1 + .../checkouts/reitit-frontend | 1 + .../checkouts/reitit-schema | 1 + examples/frontend-controllers/project.clj | 52 ++++++++++ .../resources/public/index.html | 10 ++ .../src/frontend/core.cljs | 95 +++++++++++++++++++ examples/frontend/src/frontend/core.cljs | 24 ++--- .../src/reitit/frontend/controllers.cljs | 6 +- .../src/reitit/frontend/history.cljs | 8 +- 9 files changed, 182 insertions(+), 16 deletions(-) create mode 120000 examples/frontend-controllers/checkouts/reitit-core create mode 120000 examples/frontend-controllers/checkouts/reitit-frontend create mode 120000 examples/frontend-controllers/checkouts/reitit-schema create mode 100644 examples/frontend-controllers/project.clj create mode 100644 examples/frontend-controllers/resources/public/index.html create mode 100644 examples/frontend-controllers/src/frontend/core.cljs 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/src/frontend/core.cljs b/examples/frontend/src/frontend/core.cljs index 82731258..f8c4f890 100644 --- a/examples/frontend/src/frontend/core.cljs +++ b/examples/frontend/src/frontend/core.cljs @@ -8,7 +8,7 @@ [schema.core :as s] [fipp.edn :as fedn])) -(def router (atom nil)) +(defonce history (atom nil)) (defn home-page [] [:div @@ -19,8 +19,8 @@ [:h2 "About frontend"] [:ul [:li [:a {:href "http://google.com"} "external link"]] - [:li [:a {:href (rfh/href @router ::foobar)} "Missing route"]] - [:li [:a {:href (rfh/href @router ::item)} "Missing route params"]]]]) + [: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) @@ -35,10 +35,10 @@ (defn current-page [] [:div [:ul - [:li [:a {:href (rfh/href @router ::frontpage)} "Frontpage"]] - [:li [:a {:href (rfh/href @router ::about)} "About"]] - [:li [:a {:href (rfh/href @router ::item {:id 1})} "Item 1"]] - [:li [:a {:href (rfh/href @router ::item {:id 2} {:foo "bar"})} "Item 2"]]] + [: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])) @@ -62,10 +62,12 @@ :data {:coercion rsc/coercion}})) (defn init! [] - (reset! router (rfh/start! routes - (fn [m] (reset! match m)) - {:use-fragment true - :path-prefix "/"})) + (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/modules/reitit-frontend/src/reitit/frontend/controllers.cljs b/modules/reitit-frontend/src/reitit/frontend/controllers.cljs index fe97f675..8556314d 100644 --- a/modules/reitit-frontend/src/reitit/frontend/controllers.cljs +++ b/modules/reitit-frontend/src/reitit/frontend/controllers.cljs @@ -22,9 +22,9 @@ 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))) + (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) diff --git a/modules/reitit-frontend/src/reitit/frontend/history.cljs b/modules/reitit-frontend/src/reitit/frontend/history.cljs index 97c135b1..7e178723 100644 --- a/modules/reitit-frontend/src/reitit/frontend/history.cljs +++ b/modules/reitit-frontend/src/reitit/frontend/history.cljs @@ -57,7 +57,11 @@ (.replaceToken history (path->token history (.getPath uri))))))) (defn start! - "Parameters: + "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. @@ -94,7 +98,7 @@ :close-fn (fn [] (e/unlistenByKey event-key) (e/unlistenByKey click-listen-key) - (.setEnabled history false))})) + (.dispose history))})) (defn stop! [{:keys [close-fn]}] (if close-fn From 73b1efd5b3c92e1d832cdf3073cb934c7eb1c180 Mon Sep 17 00:00:00 2001 From: Juho Teperi Date: Wed, 11 Jul 2018 13:27:30 +0300 Subject: [PATCH 18/19] Initialize empty frontend docs --- doc/README.md | 5 ++++- doc/cljdoc.edn | 5 +++++ doc/frontend/README.md | 5 +++++ doc/frontend/basics.md | 3 +++ doc/frontend/browser.md | 3 +++ doc/frontend/controllers.md | 3 +++ 6 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 doc/frontend/README.md create mode 100644 doc/frontend/basics.md create mode 100644 doc/frontend/browser.md create mode 100644 doc/frontend/controllers.md diff --git a/doc/README.md b/doc/README.md index 382b4b0c..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,8 +20,9 @@ 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.4-SNAPSHOT"] @@ -35,6 +37,7 @@ Optionally, the parts can be required separately: [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 From 50242232809bb1b152b186d401078f27292a31e0 Mon Sep 17 00:00:00 2001 From: Juho Teperi Date: Wed, 11 Jul 2018 13:33:24 +0300 Subject: [PATCH 19/19] Fix capture-console on node --- test/cljs/reitit/frontend/test_utils.cljs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/test/cljs/reitit/frontend/test_utils.cljs b/test/cljs/reitit/frontend/test_utils.cljs index 8f57f549..b70c5f37 100644 --- a/test/cljs/reitit/frontend/test_utils.cljs +++ b/test/cljs/reitit/frontend/test_utils.cljs @@ -2,18 +2,14 @@ (defn capture-console [f] (let [messages (atom []) - original-console js/console + original-console-warn js/console.warn log (fn [t & message] (swap! messages conj {:type t :message message})) value (try - (set! js/console #js {:log (partial log :log) - :warn (partial log :warn) - :info (partial log :info) - :error (partial log :error) - :debug (partial log :debug)}) + (set! js/console.warn (partial log :warn)) (f) (finally - (set! js/console original-console)))] + (set! js/console.warn original-console-warn)))] {:value value :messages @messages}))